在C语言中计算方阵的上Hessenberg形:避免昂贵的矩阵乘法
你说得太对了——用Householder变换转上Hessenberg形时,全尺寸矩阵乘法确实是算力浪费重灾区,毕竟随着i增大,矩阵前i行和列已经是符合上Hessenberg要求的结构,完全不需要再碰它们。而且Householder变换本身有非常规整的结构,完全可以绕开显式构造变换矩阵P,用更轻量的向量运算替代全矩阵乘法。
我给你拆解下具体的优化思路和代码调整方案:
核心原理:用好Householder变换的代数形式
你代码里构造的Householder变换P,其实可以写成数学表达式:P = I - 2*v*v^T/(v^T*v),其中v就是你算出来的Householder向量。当我们要做相似变换H = P*H_old*P时,完全不用真的构造P再做乘法,而是把这个矩阵运算拆成两步向量级的操作,每一步都只处理矩阵中需要更新的部分:
- 先算
tmp = H_old * P,等价于tmp = H_old - 2*(H_old*v)*v^T/(v^T*v) - 再算
H = P*tmp,等价于H = tmp - 2*v*(v^T*tmp)/(v^T*v)
这样一来,原本O(n³)的矩阵乘法,就拆成了几个O(n²)的向量-矩阵/矩阵-向量运算,而且还能跳过所有和前i行/列无关的0块运算,常数项直接降下来一大截。
针对你代码的具体优化步骤
我们直接在原矩阵上操作(不用额外开tmp数组也能行,不过分步写更清晰),而且只处理矩阵右下角的子块——毕竟前i行/列已经是上Hessenberg形,动它们纯属浪费:
1. 扔掉全尺寸的P矩阵
你代码里花了好大一通构造全尺寸的P,其实完全没必要。Householder变换只影响从第i行/列开始的子矩阵,前面的部分都是单位矩阵的块,根本不用管。
2. 用向量运算替代H*P
把你原来构造tmp = H*P的三重循环,替换成下面的逻辑:
// 计算 w = H的相关子块 乘以 Householder向量v double w[d]; for (int a = 0; a < d; a++) { w[a] = 0.0; // 只处理从i列开始的部分,前面的列不受变换影响 for (int c = i; c < d; c++) { w[a] += H[a][c] * vec[c - i]; } } // 提前把系数2/(v^T*v)乘进去,ins是你代码里的1/(v^T*v) double coeff = 2 * ins; for (int a = 0; a < d; a++) { w[a] *= coeff; } // 更新H的对应子块:H = H - w*v^T for (int a = 0; a < d; a++) { for (int b = i; b < d; b++) { H[a][b] -= w[a] * vec[b - i]; } }
3. 用向量运算替代P*tmp
同样,把H = P*tmp的三重循环替换成:
// 计算 u = v^T 乘以 H的相关子块 double u[d]; for (int b = 0; b < d; b++) { u[b] = 0.0; // 只处理从i行开始的部分 for (int c = i; c < d; c++) { u[b] += vec[c - i] * H[c][b]; } } // 同样提前乘系数 for (int b = 0; b < d; b++) { u[b] *= coeff; } // 更新H的对应子块:H = H - v*u^T for (int a = i; a < d; a++) { for (int b = 0; b < d; b++) { H[a][b] -= vec[a - i] * u[b]; } }
额外的小细节优化
- 你代码里同时存了
A和H,如果不需要保留原始矩阵的话,直接在H上原地修改就行,能省不少内存。 - 计算Householder向量的平方和时,你已经算过一次
ins,完全不用重复循环计算,直接复用就好。 - 计算向量范数的时候,可以把平方和的计算和向量初始化合并成一个循环,少跑一次循环也是赚。
优化后的效果
整体复杂度虽然还是O(d³),但常数项至少能降到原来的1/3甚至更低——因为我们跳过了所有和0块的无效运算,而且用更简单的向量运算替代了大量矩阵元素的乘法。尤其是当矩阵尺寸d比较大的时候,这个速度提升会特别明显。
备注:内容来源于stack exchange,提问作者uran42

