大学の演習で逆行列を求めるプログラムを作る課題があったんですが、「2次」と決められていたのでめんどくさくてたすき掛けで作って提出してしまいました。
それだけではつまらないので、3次以上でも適用できるメソッドを実装してみることにしました。
候補は次の2つです。
- LU分解
- 掃き出し法
今回はLU分解を選択し、これを実装することにします。掃き出し法はまた今度。
[12/29追記] 掃き出し法についても記事を書きました。
逆行列を求めるプログラムの考察 (2) — 掃き出し法
ソースを示す前にLU分解について軽く説明します。
原理
LU分解とは、\(n\) 次正方行列 \(A\) があったときに、適当な上三角行列 \(U\) と下三角行列 \(L\) があって \(A=LU\) と分解できるという方法です。
\( \left( \begin{array}{ccc} a_{11} & \ldots & a_{1n} \\ \vdots & \ddots & \vdots \\ a_{n1} & \ldots & a_{nn} \end{array} \right) = \left( \begin{array}{ccc} l_{11} & \ldots & 0 \\ \vdots & \ddots & \vdots \\ l_{n1} & \ldots & l_{nn} \end{array} \right) \left( \begin{array}{ccc} u_{11} & \ldots & u_{1n} \\ \vdots & \ddots & \vdots \\ 0 & \ldots & u_{nn} \end{array} \right) \)もちろん \(n\) 次の行列なので、式は \(n^2\) 個、未知係数は \(n(n+1)\) 個あるのでこのままでは解けません。そこで、次の2つの方法を利用します。
- Doolittle 法
- \(L\) の対角成分すべてを 1 とおく。
- Crout 法
- \(U\) の対角成分すべてを 1 とおく。
Doolittle 法を例にとって具体的な操作手順を紹介します。
- \(L\) の対角成分すべてを 1 とおく。 \( L = \left( \begin{array}{ccc} l_{11} & \ldots & 0 \\ \vdots & \ddots & \vdots \\ l_{n1} & \ldots & l_{nn} \end{array} \right) \)
- \(A=LU\) の両辺の係数を比較する。 \( \begin{cases} a_{1k} = u_{1k} \\ \vdots \end{cases} \) めんどくさいから省略しました。
- 上から順番に解いていくと、\(a_{ij}\) が決定される。
しかし、逆行列の計算をする上で、\(LU\) の対角成分に 0 があるととても困ります。そこで、対角成分に 0 があるときは 0 のない行と入れ替え、置換行列 \(P\) にその情報を覚えさせておきます。そうすればもとの行列はあとから復元可能です(注:今回は逆行列の計算しか行わないので不要ですが、連立1次方程式の解を求める場合は \(P\) を持っておくことで解が入れ替わってしまう可能性を排除できます)。
なお、以下では特に注記がなければ行列の添字(1, 2, …, \(n\))をもとに話を進めます。配列の添字については i
というように等幅フォントで明示します。
ソース
C言語で実装した例を次に挙げます。なお、main 関数以外はほとんど C# – 行列分解 をC#から移植しただけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
#include <math.h> #include <memory.h> #define SIZE 4 // たとえば4次 typedef struct { double data[SIZE][SIZE]; } Matrix; typedef struct { double data[SIZE]; } bvector; Matrix MatrixCreate(); Matrix MatrixInput(); Matrix MatrixInverse(Matrix X, int* error); Matrix MatrixDuplicate(Matrix X); bvector bvectorDuplicate(double X[SIZE]); Matrix MatrixDecompose(Matrix X, bvector* P, int* toggle, int* error); bvector HelperSolve(Matrix LU, bvector b); int MatrixPrint(Matrix X); double epsilon = 1e-10; int main() { int i, j, t, err; Matrix A, I; bvector perm; err = 0; printf("%d 次正方行列の逆行列を求めるプログラムです\n", SIZE); A = MatrixInput(); I = MatrixCreate(); I = MatrixInverse(A, &err); if (err) { printf("\n逆行列は存在しません\n"); return 1; } else { printf("\n逆行列は\n"); MatrixPrint(I); } } Matrix MatrixCreate() { Matrix X; int i, j; for (i=0;i<SIZE;i++) { for (j=0;j<SIZE;j++) { X.data[i][j] = 0.0; } } return X; } Matrix MatrixInput() { Matrix X; int i, j; for (i=0;i<SIZE;i++) { for (j=0;j<SIZE;j++) { printf("%d 次正方行列Aの (%d,%d) 成分を入力してください\n", SIZE, i+1, j+1); scanf("%lf", &X.data[i][j]); } } return X; } Matrix MatrixInverse(Matrix X, int* error) { Matrix R, LUM; int i, j, toggle, err; bvector perm, b, x; err = 0; R = MatrixDuplicate(X); LUM = MatrixDecompose(X, &perm, &toggle, &err); if (err) { *error = 1; R = MatrixCreate(); } else { for (i=0;i<SIZE;i++) { for (j=0;j<SIZE;j++) { if (i == perm.data[j]) b.data[j] = 1; else b.data[j] = 0; } x = HelperSolve(LUM, b); for (j=0;j<SIZE;j++) R.data[j][i] = x.data[j]; } } return R; } Matrix MatrixDuplicate(Matrix X) { Matrix Y; int i, j; for (i=0;i<SIZE;i++) { for (j=0;j<SIZE;j++) { Y.data[i][j] = X.data[i][j]; } } return Y; } Matrix MatrixDecompose(Matrix X, bvector* P, int* toggle, int* error) { Matrix R = MatrixDuplicate(X); int i, j, k, row, temp; double colMax, rowPtr; for (i=0;i<SIZE;i++) { (*P).data[i] = i; } *toggle = 1; for (j=0;j<SIZE-1;j++) { colMax = fabs(R.data[j][j]); row = j; for (i=j+1;i<SIZE;i++) { if (R.data[i][j] > colMax) { colMax = R.data[i][j]; row = i; } } if (row != j) { for (k=0;k<SIZE;k++) { rowPtr = R.data[row][k]; R.data[row][k] = R.data[j][k]; R.data[j][k] = rowPtr; } temp = (*P).data[row]; (*P).data[row] = (*P).data[j]; (*P).data[j] = temp; *toggle = -(*toggle); } if (fabs(R.data[j][j]) < epsilon) { *error = 1; } else { for (i=j+1;i<SIZE;i++) { R.data[i][j] /= R.data[j][j]; for (k=j+1;k<SIZE;k++) { R.data[i][k] -= R.data[i][j] * R.data[j][k]; } } } } return R; } bvector HelperSolve(Matrix LU, bvector b) { int i, j; double sum; bvector x; for (i=0;i<SIZE;i++) { x.data[i] = b.data[i]; } for (i=1;i<SIZE;i++) { sum = x.data[i]; for (j=0;j<i;j++) { sum -= LU.data[i][j] * x.data[j]; } x.data[i] = sum; } x.data[SIZE-1] /= LU.data[SIZE-1][SIZE-1]; for (i=SIZE-2;i>=0;i--) { sum = x.data[i]; for (j=i+1;j<SIZE;j++) { sum -= LU.data[i][j] * x.data[j]; } x.data[i] = sum / LU.data[i][i]; } return x; } int MatrixPrint(Matrix X) { int i, j; for (i=0;i<SIZE;i++) { for (j=0;j<SIZE;j++) { printf("% lf\t", X.data[i][j]); } printf("\n"); } return 0; } |
C#と違い、C言語は多次元配列をスムーズに扱えるように設計されていません。そのため、\(n\) 次正方行列を Matrix
、\(n\) 次列ベクトルを bvector
という構造体を定義して扱っています。
それでは各部分の動作を見てみましょう。
MatrixCreate
で行列を初期化します。A
は入力行列、I
は逆行列です。 MatrixInput
でキーボードから行列 A
を入力させます。
MatrixInverse
で逆行列を求め、MatrixPrint
で表示しています。なお、MatrixInverse
に渡した変数 err
が 1
になっている場合は \(\det A = 0\) ですから、逆行列が存在しない旨を表示して終了します。
LUP分解と逆行列の計算
MatrixInverse
とそれに関係するコードを深く掘り下げてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
Matrix MatrixInverse(Matrix X, int* error) { Matrix R, LUM; int i, j, toggle, err; bvector perm, b, x; err = 0; R = MatrixDuplicate(X); LUM = MatrixDecompose(X, &perm, &toggle, &err); if (err) { *error = 1; R = MatrixCreate(); } else { for (i=0;i<SIZE;i++) { for (j=0;j<SIZE;j++) { if (i == perm.data[j]) b.data[j] = 1; else b.data[j] = 0; } x = HelperSolve(LUM, b); for (j=0;j<SIZE;j++) R.data[j][i] = x.data[j]; } } return R; } Matrix MatrixDuplicate(Matrix X) { Matrix Y; int i, j; for (i=0;i<SIZE;i++) { for (j=0;j<SIZE;j++) { Y.data[i][j] = X.data[i][j]; } } return Y; } Matrix MatrixDecompose(Matrix X, bvector* P, int* toggle, int* error) { Matrix R = MatrixDuplicate(X); int i, j, k, row, temp; double colMax, rowPtr; for (i=0;i<SIZE;i++) { (*P).data[i] = i; } *toggle = 1; for (j=0;j<SIZE-1;j++) { colMax = fabs(R.data[j][j]); row = j; for (i=j+1;i<SIZE;i++) { if (R.data[i][j] > colMax) { colMax = R.data[i][j]; row = i; } } if (row != j) { for (k=0;k<SIZE;k++) { rowPtr = R.data[row][k]; R.data[row][k] = R.data[j][k]; R.data[j][k] = rowPtr; } temp = (*P).data[row]; (*P).data[row] = (*P).data[j]; (*P).data[j] = temp; *toggle = -(*toggle); } if (fabs(R.data[j][j]) < epsilon) { *error = 1; } else { for (i=j+1;i<SIZE;i++) { R.data[i][j] /= R.data[j][j]; for (k=j+1;k<SIZE;k++) { R.data[i][k] -= R.data[i][j] * R.data[j][k]; } } } } return R; } bvector HelperSolve(Matrix LU, bvector b) { int i, j; double sum; bvector x; for (i=0;i<SIZE;i++) { x.data[i] = b.data[i]; } for (i=1;i<SIZE;i++) { sum = x.data[i]; for (j=0;j<i;j++) { sum -= LU.data[i][j] * x.data[j]; } x.data[i] = sum; } x.data[SIZE-1] /= LU.data[SIZE-1][SIZE-1]; for (i=SIZE-2;i>=0;i--) { sum = x.data[i]; for (j=i+1;j<SIZE;j++) { sum -= LU.data[i][j] * x.data[j]; } x.data[i] = sum / LU.data[i][i]; } return x; } |
MatrixDecompose
でLUP分解を行います。
- 置換行列 \(P\) は理論上は \(n\) 次正方行列ですが、ここでは入れ替えた行が追跡できれば目的を満たすので、第 \(i\) 成分が \(i\) である \(n\) 次ベクトルになっています。
toggle
は線形代数学でいうところの「置換の符号」です。1 ならば偶置換(すなわち、行入れ替えが偶数回行われた)、-1 ならば奇置換です。これは行列式を求める場合に使います。- 各列について、対角成分の絶対値が最大となるように入れ替えを行います。
colMax
に初めの対角成分の絶対値を格納し、以降の行でそれより大きい成分があればその行をrow
に記録しておいて、探索が終わったらrow
行と入れ替えます。 - 行を入れ替えても対角成分が 0 ならば逆行列は存在しないので、
error
フラグを立てて終了します。単に行列分解を行う(逆行列を求めることを目的としない)ならばこの部分は必要ありません。 - 対角成分が 0 でないならば、分解を行います。ここから先は少々複雑です。
- \(A=LU\) の係数を比較することで、次の方程式を得ます。\( a_{ij} = \sum_{k=1}^n l_{ik} u_{kj} \)
- いま、\(j=1\) とします。\(a_{11} = l_{11} u_{11} = u_{11}\) で、\(i \geq 2\) に対し \(a_{i1} = l_{i1} u_{11} = l_{i1} a_{11}\) ですから、\(a_{i1} / a_{11}\) を実行すれば A の第1列は L の第1列と((1,1) 成分以外は)等しくなります。
- さらに、\(a_{1k} = l_{11} u_{1k} = u_{1k}\) を用いると、\(a_{2k} = l_{21} u_{1k} + u_{2k}\) ですから、\(a_{2k} – (a_{i1} / a_{11}) a_{1k}\) を実行すれば、Aの第2列は U の第2列と((2,1) 成分以外は)等しくなります。
- これらを順次実行していけば、\( \left( \begin{array}{cccc} u_{11} & u_{12} & \cdots & u_{1n} \\ l_{21} & u_{22} & \cdots & u_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ l_{n1} & l_{n2} & \cdots & u_{nn} \end{array} \right) \) のような行列が完成します。
逆行列の計算――連立1次方程式の解
つづいて、HelperSolve
で連立1次方程式の解を求めます。逆行列を求めるのに連立1次方程式?と思うかもしれませんが、置換行列の第 \(i\) 列を \(\vec{b}\) として、\(A\vec{x}=\vec{b}\) なる \(\vec{b}\) を求め、これを逆行列 \(I\) の第 \(i\) 列とするということです。
\(\vec{b}\) を作るには、置換行列 \(P\) から第 \(i\) 列を取り出します。つまり、perm[j]
が i
ならば、\(\vec{b}\) の第 \(j\) 成分は 1、そうでなければ 0 とすればよいのです。たとえば i=0
で、 perm[j] = {2, 1, 0, 3}
なら、\( \vec{b} = \left( \begin{array}{c} 0 \\ 0 \\ 1 \\ 0 \end{array} \right) \) です。
\(A=LU\) ですから、\(A\vec{x}=\vec{b}\) はすなわち \(LU\vec{x}=\vec{b}\) です。ここで、\(U\vec{x}=\vec{c}\) とすると、\(L\vec{c}=\vec{b}\) ですから、まず \(\vec{c}\) を求め、次に \(\vec{c}\) を用いて \(\vec{x}\) を求めます。これが HelperSolve
の主な処理です。
詳しく見てみましょう。
- \(\vec{x}\) に \(\vec{b}\) を代入します。
- まず \(L\vec{c}=\vec{b}\) を解きます。\(l_{11} c_1 = c_1 = b_1\) です。次に、\(l_{21} c_1 + l_{22} c_2 = l_{21} c_1 + c_2 = b_2\) ですから、\(c_2 = b_2 – A_{21} b_1\) として計算できます。以下、前の計算で求まった \(c_i\) を次の方程式に代入していきます(これを前進代入といいます)。
- 前進代入が終わったら、\(U\vec{x} = \vec{c}\) を解きます。まず、\(u_{nn} x_n = c_n\) ですから、\(x_n = c_n / A_{nn}\) です。次に、\(u_{n-1,n-1} x_{n-1} + u_{n-1,n} x_n = c_{n-1}\) ですから、\(x_{n-1} = (c_{n-1} – A_{n-1,n} x_n) / A_{n-1,n-1}\) として計算できます。以下、前の計算で求まった \(x_i\) を次の方程式に代入していきます(これを後退代入といいます)。
以上の処理を終えると、MathInverse
内で得られた R
が求める逆行列ということになります。
感想
私がなぜ演習の課題としてこれを提出するのを諦めたかというと、これだけ洗練されたプログラムは自分一人では到底書けなかったからです。実際、このプログラムを移植したはいいものの、仕組みを理解するのにかなり時間がかかりました。
本記事は線形代数学(特に行列の演算)についてある程度の知識を必要とします。詳しく学びたい方は 線型代数のビブリオグラフィー [数学についてのwebノート] を参考に、大きな書店や図書館で参考書を探してみてください。ちなみに私は『理系のための線型代数の基礎』で学びました。
掃き出し法の方も出来上がったら記事を書きます。