LU 分解和矩阵实现
这一节我们实现矩阵运算。矩阵的乘法等操作都是很好实现的,唯独矩阵的逆比较复杂。之前大学线性代数的课程中,矩阵求逆都是用解线性方程组的方式进行手算的。但是觉得“模拟”的方式不太适合计算机运行,特别涉及行交换的时候。
查资料选了 LU 分解的方式求逆矩阵。选择它的原因是,LU 分解本质和手算使用的高斯消去法是一样的,让我觉得应该不会太难,先够用就行。
当然,空间变换时维度低的矩阵进行求逆,可以直接使用伴随矩阵的方法。不过为了通用性,决定采用 LU 分解实现求逆。
1. 高斯消去法:矩阵变换
我们先以矩阵变换的角度来表示高斯消去法。比如,矩阵使用高斯消去法,首先消去第一列,即:第一行乘以 -2 加到第二行,第一行乘以 -4 加到第三行,第一行乘以 -3 加到第四行。写成矩阵变换的形式为:
\( L_1A= \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ -2 & 1 & \phantom{0} & \phantom{0} \\ -4 & \phantom{0} & 1 & \phantom{0}\\ -3 & \phantom{0} & \phantom{0} & 1 \end{pmatrix} \begin{pmatrix} 2 & 1 & 1 & 0 \\ 4 & 3 & 3 & 1 \\ 8 & 7 & 9 & 5\\ 6 & 7 & 9 & 8 \end{pmatrix} = \begin{pmatrix} 2 & 1 & 1 & 0 \\ 0 & 1 & 1 & 1 \\ 0 & 3 & 5 & 5\\ 0 & 4 & 6 & 8 \end{pmatrix} \)
同理,消去第二列:
\( L_2(L_1A)= \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ \phantom{0} & 1 & \phantom{0} & \phantom{0} \\ \phantom{0} & -3 & 1 & \phantom{0}\\ \phantom{0} & -4 & \phantom{0} & 1 \end{pmatrix} \begin{pmatrix} 2 & 1 & 1 & 0 \\ 0 & 1 & 1 & 1 \\ 0 & 3 & 5 & 5\\ 0 & 4 & 6 & 8 \end{pmatrix} = \begin{pmatrix} 2 & 1 & 1 & 0 \\ 0 & 1 & 1 & 1 \\ 0 & 0 & 2 & 2\\ 0 & 0 & 2 & 4 \end{pmatrix} \)
同理,消去第三列:
\( L_3(L_2L_1A)= \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ \phantom{0} & 1 & \phantom{0} & \phantom{0} \\ \phantom{0} & \phantom{0} & 1 & \phantom{0}\\ \phantom{0} & \phantom{0} & -1 & 1 \end{pmatrix} \begin{pmatrix} 2 & 1 & 1 & 0 \\ 0 & 1 & 1 & 1 \\ 0 & 0 & 2 & 2\\ 0 & 0 & 2 & 4 \end{pmatrix} = \begin{pmatrix} 2 & 1 & 1 & 0 \\ 0 & 1 & 1 & 1 \\ 0 & 0 & 2 & 2\\ 0 & 0 & 0 & 2 \end{pmatrix} \)
这边最终的消去结果就是 \(U\)。就是上三角矩阵的缩写,Upper triangular matrix。
现在我们看一下 \(L_1\)、\(L_2\)、\(L_3\) 的性质。\(L_1\) 的逆为:
\( L_1^{-1}= \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ 2 & 1 & \phantom{0} & \phantom{0} \\ 4 & \phantom{0} & 1 & \phantom{0}\\ 3 & \phantom{0} & \phantom{0} & 1 \end{pmatrix} \)
可以看到是“系数是相反数”的形式。同样 \(L_2\)、\(L_3\) 也是。
直观上也容易理解:第二行已经加上了第一行乘以 -2 的值,现在求逆,即需要把它恢复回去,只需要第二行再加上了第一行乘以 2 的值。
我们再看一个很好的性质:
\(L_1^{-1}L_2^{-1}= \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ 2 & 1 & \phantom{0} & \phantom{0} \\ 4 & 3 & 1 & \phantom{0}\\ 3 & 4 & \phantom{0} & 1 \end{pmatrix} \)
可以发现就是“内容的叠加”。
直观上也容易理解:\(L_1^{-1}L_2^{-1}\) 就是 \(L_2^{-1}\) 作用上 \(L_1^{-1}\) 的行变化。
\(L_1^{-1}\) 只有第一行中,只有第一列不为0,所以变化只会影响到 \(L_2^{-1}\) 的第一列。
而 \(L_2^{-1}\) 的第一列全为 0,所以反应在结果上就是“内容的叠加”。
同样 \(L_1^{-1}L_2^{-1}L_3^{-1}\) 也是这样。现在我们已经有
\(L_3L_2L_1A=U\)
用逆做变换
\(L_1^{-1}L_2^{-1}L_3^{-1}L_3L_2L_1A=L_1^{-1}L_2^{-1}L_3^{-1}U\)
\(A=L_1^{-1}L_2^{-1}L_3^{-1}U\)
这边的 \(L_1^{-1}L_2^{-1}L_3^{-1}\),我们记作 \(L\),即下三角矩阵的缩写,Lower triangular matrix。这就是 LU 分解名称的由来,它把一个矩阵分解成下三角和上三角矩阵相乘的形式。
1.1 LU 分解的代码实现
因为 LU 分解的性质非常好,代码实现也很简洁。如代码清单 1 所示,U 基于原矩阵,做高斯消去;L 基于单位矩阵,记录各个高斯消去系数。
- void LUDecomposition(Matrix* L, Matrix* U)
- {
- assert(ROWS == COLS);
- *U = *this;
- L->ToIdentity();
- for (size_t k = 0; k < ROWS; k++)
- {
- for (size_t i = k + 1; i < ROWS; i++)
- {
- assert(U->at(k, k) != 0);
- L->at(i, k) = U->at(i, k) / U->at(k, k);
- for (size_t j = k; j < ROWS; j++)
- U->at(i, j) -= L->at(i, k) * U->at(k, j);
- }
- }
- }
2. PLU 分解
之前的 LU 分解还有一些缺陷。比如,对角线上有元素为 0,0 乘以任何数都为 0,消去不了任何项;对角线上的元素很小,消去的时候大数除以小数,影响精度。
针对以上问题,我们可以改进选择消去法时主元的策略:我们可以选取绝对值最大的数当作主元。
我们看到例子。首先交换第一行和第三行,有置换矩阵 \(P_1\)
\( \begin{pmatrix} \phantom{0} & \phantom{0} & 1 & \phantom{0}\\ \phantom{0} & 1 & \phantom{0} & \phantom{0} \\ 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ \phantom{0} & \phantom{0} & \phantom{0} & 1 \end{pmatrix} \begin{pmatrix} 2 & 1 & 1 & 0 \\ 4 & 3 & 3 & 1 \\ 8 & 7 & 9 & 5\\ 6 & 7 & 9 & 8 \end{pmatrix} = \begin{pmatrix} 8 & 7 & 9 & 5 \\ 4 & 3 & 3 & 1 \\ 2 & 1 & 1 & 0 \\ 6 & 7 & 9 & 8 \end{pmatrix} \)
置换矩阵形式上就是单位矩阵交换行。
接着进行消元,有变换矩阵 \(L_1\)
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ -\frac{1}{2} & 1 & \phantom{0} & \phantom{0} \\ -\frac{1}{4} & \phantom{0} & 1 & \phantom{0}\\ -\frac{3}{4} & \phantom{0} & \phantom{0} & 1 \end{pmatrix} \begin{pmatrix} 8 & 7 & 9 & 5 \\ 4 & 3 & 3 & 1 \\ 2 & 1 & 1 & 0 \\ 6 & 7 & 9 & 8 \end{pmatrix} = \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & -\frac{1}{2} & -\frac{3}{2} & -\frac{3}{2} \\ 0 & -\frac{3}{4} & -\frac{5}{4} & -\frac{5}{4} \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \end{pmatrix} \)
接着交换第二行和第四行交换,有置换矩阵 \(P_2\)
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ \phantom{0} & \phantom{0} & \phantom{0} & 1 \\ \phantom{0} & \phantom{0} & 1 & \phantom{0}\\ \phantom{0} & 1 & \phantom{0} & \phantom{0} \end{pmatrix} \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & -\frac{1}{2} & -\frac{3}{2} & -\frac{3}{2} \\ 0 & -\frac{3}{4} & -\frac{5}{4} & -\frac{5}{4} \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \end{pmatrix} = \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \\ 0 & -\frac{3}{4} & -\frac{5}{4} & -\frac{5}{4} \\ 0 & -\frac{1}{2} & -\frac{3}{2} & -\frac{3}{2} \end{pmatrix} \)
接着进行消元,有变换矩阵 \(L_2\)
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ \phantom{0} & 1 & \phantom{0} & \phantom{0} \\ \phantom{0} & \frac{3}{7} & 1 & \phantom{0}\\ \phantom{0} & \frac{2}{7} & \phantom{0} & 1 \end{pmatrix} \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \\ 0 & -\frac{3}{4} & -\frac{5}{4} & -\frac{5}{4} \\ 0 & -\frac{1}{2} & -\frac{3}{2} & -\frac{3}{2} \end{pmatrix} = \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \\ 0 & 0 & -\frac{2}{7} & \frac{4}{7} \\ 0 & 0 & -\frac{6}{7} & -\frac{2}{7} \end{pmatrix} \)
接着交换第三行和第四行交换,有置换矩阵 \(P_3\)
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ \phantom{0} & 1 & \phantom{0} & \phantom{0} \\ \phantom{0} & \phantom{0} & \phantom{0} & 1\\ \phantom{0} & \phantom{0} & 1 & \phantom{0} \end{pmatrix} \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \\ 0 & 0 & -\frac{2}{7} & \frac{4}{7} \\ 0 & 0 & -\frac{6}{7} & -\frac{2}{7} \end{pmatrix} = \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \\ 0 & 0 & -\frac{6}{7} & -\frac{2}{7} \\ 0 & 0 & -\frac{2}{7} & \frac{4}{7} \end{pmatrix} \)
接着进行消元,有变换矩阵 \(L_3\)
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0}\\ \phantom{0} & 1 & \phantom{0} & \phantom{0} \\ \phantom{0} & \phantom{0} & 1 & \phantom{0}\\ \phantom{0} & \phantom{0} & -\frac{1}{3} & 1 \end{pmatrix} \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \\ 0 & 0 & -\frac{6}{7} & -\frac{2}{7} \\ 0 & 0 & -\frac{2}{7} & \frac{4}{7} \end{pmatrix} = \begin{pmatrix} 8 & 7 & 9 & 5 \\ 0 & \frac{7}{4} & \frac{9}{4} & \frac{17}{4} \\ 0 & 0 & -\frac{6}{7} & -\frac{2}{7} \\ 0 & 0 & 0 & \frac{2}{3} \end{pmatrix} \)
最终的形式如下
\(L_3P_3L_2P_2L_1P_1A=U\)
但是以上这形式,既不确定 \(L_3P_3L_2P_2L_1P_1\) 是否是下三角矩阵,也不好求逆。因为 \(P^{-1}=P\),我们重新构造一下:
\(L_3P_3L_2(P_3P_3)P_2L_1(P_2P_3P_3P_2)P_1A=U\)
重新组合一下
\(L_3(P_3L_2P_3)(P_3P_2L_1P_2P_3)(P_3P_2P_1)A=U\)
我们令
\(P=P_3P_2P_1\)
\(\widetilde{L}_2=P_3L_2P_3\)
\(\widetilde{L}_1=P_3P_2L_1P_2P_3\)
这边 \(\widetilde{L}_1\) 、\(\widetilde{L}_2\) 的形式和 \(L_1\) 、\(L_2\) 的形式是一样的。我们以 \(\widetilde{L}_1\) 为例子感受一下。
左乘是行交换,右乘是列交换。\(P_2\) 作用于第二行和第四行,首先行交换,得
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0} \\ -\frac{3}{4} & \phantom{0} & \phantom{0} & 1 \\ -\frac{1}{4} & \phantom{0} & 1 & \phantom{0}\\ -\frac{1}{2} & 1 & \phantom{0} & \phantom{0} \end{pmatrix} \)
再交换列,得
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0} \\ -\frac{3}{4} & 1 & \phantom{0} & \phantom{0} \\ -\frac{1}{4} & \phantom{0} & 1 & \phantom{0}\\ -\frac{1}{2} & \phantom{0} & \phantom{0} & 1 \end{pmatrix} \)
可以发现又重新变成了行变化的形式,只是第一列中局部的行进行了交换。
\(P_3\) 作用于第三行和第四行。同理,交换行列,得
\( \begin{pmatrix} 1 & \phantom{0} & \phantom{0} & \phantom{0} \\ -\frac{3}{4} & 1 & \phantom{0} & \phantom{0} \\ -\frac{1}{2} & \phantom{0} & 1 & \phantom{0}\\ -\frac{1}{4} & \phantom{0} & \phantom{0} & 1 \end{pmatrix} \)
同样也只是第一列中局部的行进行了交换。一般的有
\(\widetilde{L}_k=P_{n-1}\cdots P_{k+1}L_kP_{k+1}\cdots P_{n-1}\)
从下标中我们可以感受到,因为是方阵,\(L_k\) 对于比它下标大的操作,比如 \(P_{k+1}\),对于行索引,作用范围肯定是大于 k。这就表示,作用不到单位矩阵对角线上的最小索引的 1 位置;因为行变换,破坏了对角线上 1 的位置,但是列变化也会换过来,对于列索引,同样作用范围肯定是大于 k,这时除了对角线上的 1 以外,其余都是 0,并不影响最重要的 k 列结果。
我不知道这个证明的书面表示是怎么样的。但是感觉严谨的公式表达也会非常繁琐。
有时候有的问题要“站得高处”思考更容易;有时候有的问题,像此处,反而以特例试验一下,更容易理解。
不过也只是方便理解。“第一个人”是怎么从无到有想到这种组合方式的,还是非常好奇。
至此,PLU 分解就说明完毕了,我们记
\(L=\widetilde{L}_1^{-1}\widetilde{L}_2^{-1}L_3^{-1}\)
得
\(PA=LU\)
2.1 PLU 分解的代码实现
已经知道了具体的步骤,我们来看如何代码实现。如代码清单 2 所示,第 15 至 26 行算出绝对值最大主元。
我们不用“严格按照”上述的公式一次性得到所求的值,可以逐步完成。如 \(P\),我们可以依次交换得到。同样 \(L\),我们也可以后续逐步“修正”它的位置。
- void PLUDecomposition(Matrix* P, Matrix* L, Matrix* U)
- {
- assert(ROWS == COLS);
- T maxEle, ele;
- size_t i, j, k;
- size_t maxRow;
- *U = *this;
- L->ToIdentity();
- P->ToIdentity();
- for (k = 0; k < ROWS; k++)
- {
- maxEle = std::abs(U->at(k, k));
- maxRow = k;
- for (i = k + 1; i < ROWS; i++)
- {
- ele = std::abs(U->at(i, k));
- if (ele > maxEle)
- {
- maxEle = ele;
- maxRow = i;
- }
- }
- if (k != maxRow)
- {
- U->SwapRows(k, maxRow);
- P->SwapRows(k, maxRow);
- // SwapRowsPartial
- for (j = 0; j < k; j++)
- std::swap(L->at(k, j), L->at(maxRow, j));
- }
- for (i = k + 1; i < ROWS; i++)
- {
- assert(U->at(k, k) != 0);
- L->at(i, k) = U->at(i, k) / U->at(k, k);
- for (size_t j = k; j < ROWS; j++)
- U->at(i, j) -= L->at(i, k) * U->at(k, j);
- }
- }
- }
3. 基于 PLU 分解求逆
现在我们已经可以得到 PLU 分解的结果了,我们来看如何通过它算矩阵的逆。和求解方程组一样,我们求 \(x\),使得
\(Ax=b\)
两边同乘以 \(P\),得
\(PAx=Pb\)
代入 LU 分解,得
\(LUx=Pb\)
上下三角求解是方便的,所以我们 \(Y=Ux\),即求上三角形式
\(L(Ux)=Pb \Leftrightarrow LY=Pb\)
得到 \(Y\) 后,最后再求下三角形式
\(Ux=Y\)
针对求逆的情况,我们只需要令 \(b=E\) 即可。
3.1 代码实现
首先,如代码清单 3.1 所示,我们实现上、下三角矩阵形式的方程求解。
- // LY=B
- template<size_t N>
- static void ForwardSubstitute(const Matrix& L, const Matrix<T, ROWS, N>& B, Matrix<T, COLS, N>* Y)
- {
- assert(ROWS == COLS);
- T sum;
- for (size_t i = 0; i < COLS; i++)
- {
- for (size_t j = 0; j < N; j++)
- {
- sum = B.at(i, j);
- for (size_t k = 0; k < i; k++)
- sum -= L.at(i, k) * Y->at(k, j);
- Y->at(i, j) = sum / L.at(i, i);
- }
- }
- }
- // UX=Y
- template<size_t N>
- static void BackwardSubstitute(const Matrix& U, const Matrix<T, ROWS, N>& Y, Matrix<T, COLS, N>* X)
- {
- assert(ROWS == COLS);
- T sum;
- for (int i = COLS - 1; i >= 0; i--)
- {
- for (size_t j = 0; j < N; j++)
- {
- sum = Y.at(i, j);
- for (size_t k = i + 1; k < COLS; k++)
- sum -= U.at(i, k) * X->at(k, j);
- X->at(i, j) = sum / U.at(i, i);
- }
- }
- }
矩阵求逆的过程如代码清单 3.2 所示,和我们上述介绍的过程一样,首先进行 PLU 分解,接着依次求解上、下三角矩阵形式的方程组。
- Matrix Inverse()
- {
- assert(ROWS == COLS);
- TMatrix P, L, U;
- PLUDecomposition(&P, &L, &U);
- //LY=P
- TMatrix X, Y;
- ForwardSubstitute(L, P, &Y);
- //UX=Y
- BackwardSubstitute(U, Y, &X);
- return X;
- }
最后我们写一个测试用例,如代码清单 3.3 所示,我们计算矩阵的逆,并进行打印显示。同时,我们也测试一下奇异矩阵的情况,目前我们的处理流程中会发生断言。
本章完整代码在 tag/matrix_ops。
- TMatrixOpsTask::TMatrixOpsTask()
- {
- tmath::Mat4f m = { 8, 7, 9, 5,
- 4, 3, 3, 1,
- 2, 1, 1, 0,
- 6, 7, 9, 8 };
- m.Print("m");
- tmath::Mat4f mi = m.Inverse();
- mi.Print("inverse of m");
- #if 0
- tmath::Mat4f s = { 1, 2, 3, 4,
- 2, 4, 6, 8,
- 1, 1, 1, 1,
- 0, 1, 2, 3 };
- s.Print("singular");
- tmath::Mat4f si = s.Inverse();
- #endif
- }