数値解析の講義でガウスの消去法について学習したが、実装しようと思うと手が止まってしまう。
考え方や実装例を教えて欲しい。
こんなお悩みを解決します。
今回は、数値解析を実施する上で、一度は目にすることになる「ガウスの消去法」について解説したいと思います。
これから数値計算を学習し始める学生や、再度勉強し始めた社会人の方にとって、参考になればよいと思っています。
考え方
ガウスの消去法では、行列表示された連立一次方程式の係数行列を上三角行列に変形(前進消去)し、今度は逆方向に解を求めていく(後退代入)という手法をとります。
ここでは、以下の連立一次方程式を例に取り上げ、ガウスの消去法について説明します。
$$
\begin{eqnarray*}
\left(
\begin{array}{ccc}
2 & 1 & -2\\
1 & 1 & -1\\
1 & -2 & 3\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
4\\
-1\\
\end{array}
\right)
\end{eqnarray*}
$$
前進消去
前進消去では、係数行列の対角成分より下側を0にする操作となります。
\((2, 1)\)成分の消去
\((2, 1)\)成分の消去は、以下のように計算します。
- 1列目に関して、\(\frac{(2, 1)\text{成分}}{(1, 1)\text{成分}}\)を計算します。
- 2行目から\(\frac{(2, 1)\text{成分}}{(1, 1)\text{成分}} \times 1\text{行目}\)を引きます。
ここで、\(\frac{(2, 1)\text{成分}}{(1, 1)\text{成分}} = \frac{1}{2}\)ですので、2行目を式変形すると、以下のようになります。
$$
\begin{eqnarray*}
&& \left(
\begin{array}{ccc}
2 & 1 & -2\\
1 & 1 & -1\\
1 & -2 & 3\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
4\\
-1\\
\end{array}
\right)\\
& \Leftrightarrow & \left(
\begin{array}{ccc}
2 & 1 & -2\\
1 - \frac{1}{2} \cdot 2 & 1 - \frac{1}{2} \cdot 1 & -1 - \frac{1}{2} \cdot (-2)\\
1 & -2 & 3\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
4 - \frac{1}{2} \cdot 1\\
-1\\
\end{array}
\right)\\
& \Leftrightarrow & \left(
\begin{array}{ccc}
2 & 1 & -2\\
0 & \frac{1}{2} & 0\\
1 & -2 & 3\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
\frac{7}{2} \\
-1\\
\end{array}
\right)
\end{eqnarray*}
$$
\((3, 1)\)成分の消去
同様に、\((2, 2)\)成分の消去は、以下のように計算します。
- 1列目に関して、\(\frac{(3, 1)\text{成分}}{(1, 1)\text{成分}}\)を計算します。
- 3行目から\(\frac{(3, 1)\text{成分}}{(1, 1)\text{成分}} \times 1\text{行目}\)を引きます。
ここで、\(\frac{(3, 1)\text{成分}}{(1, 1)\text{成分}} = \frac{1}{2}\)ですので、3行目を式変形すると、以下のようになります。
$$
\begin{eqnarray*}
&& \left(
\begin{array}{ccc}
2 & 1 & -2\\
0 & \frac{1}{2} & 0\\
1 & -2 & 3\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
\frac{7}{2}\\
-1\\
\end{array}
\right)\\
& \Leftrightarrow & \left(
\begin{array}{ccc}
2 & 1 & -2\\
0 & \frac{1}{2} & 0\\
1 - \frac{1}{2} \cdot 2 & -2 - \frac{1}{2} \cdot 1 & 3 - \frac{1}{2} \cdot (-2)\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
\frac{7}{2}\\
-1 - \frac{1}{2} \cdot 1\\
\end{array}
\right)\\
& \Leftrightarrow & \left(
\begin{array}{ccc}
2 & 1 & -2\\
0 & \frac{1}{2} & 0\\
0 & -\frac{5}{2} & 4\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
\frac{7}{2}\\
-\frac{3}{2}\\
\end{array}
\right)
\end{eqnarray*}
$$
\((3, 2)\)成分の消去
\((3, 2)\)成分の消去は、以下のように計算します。
- 2列目に関して、\(\frac{(3, 2)\text{成分}}{(2, 2)\text{成分}}\)を計算します。
- 3行目から\(\frac{(3, 2)\text{成分}}{(2, 2)\text{成分}} \times 2\text{行目}\)を引きます。
ここで、\(\frac{(3, 2)\text{成分}}{(2, 2)\text{成分}} = -5\)ですので、3行目を式変形すると、以下のようになります。
$$
\begin{eqnarray*}
&& \left(
\begin{array}{ccc}
2 & 1 & -2\\
0 & \frac{1}{2} & 0\\
0 & -\frac{5}{2} & 4\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
\frac{7}{2}\\
-\frac{3}{2}\\
\end{array}
\right)\\
& \Leftrightarrow & \left(
\begin{array}{ccc}
2 & 1 & -2\\
0 & \frac{1}{2} & 0\\
0 & -\frac{5}{2} - (-5) \cdot \frac{1}{2} & 4 - (-5) \cdot 0\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
\frac{7}{2}\\
-\frac{3}{2} - (-5) \cdot \frac{7}{2} \\
\end{array}
\right)\\
& \Leftrightarrow & \left(
\begin{array}{ccc}
2 & 1 & -2\\
0 & \frac{1}{2} & 0\\
0 & 0 & 4\\
\end{array}
\right)\left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
\frac{7}{2}\\
16\\
\end{array}
\right)
\end{eqnarray*}
$$
後退代入
後退代入では、行列の下から上に向かって計算することで、未知数を求めていきます。前進消去した結果をもとに、一度連立一次方程式の形に直して考えたいと思います。
連立一次方程式としての表現
連立一次方程式の形で表現した結果を以下に示します。
$$
\left\{
\begin{array}{rcc}
2 x_{1} + x_{2} -2 x_{3} & = & 1\\
\frac{1}{2}x_{2} + 0x_{3} & = & \frac{7}{2}\\
4x_{3} & = & 16\\
\end{array}
\right.
$$
\(x_{3}\)を求める
連立一次方程式の3行目に注目します。未知変数は\(x_{3}\)のみなので、\(x_{3}\)の値を求めることができます。
$$
\begin{eqnarray*}
& & 4x_{3} = 16\\
& \Leftrightarrow & x_{3} = 4
\end{eqnarray*}
$$
\(x_{2}\)を求める
連立一次方程式の2行目に注目します。\(x_{3}\)の値はすでに得られているため、未知変数\(x_{2}\)を求めることができます。
$$
\begin{eqnarray*}
& & \frac{1}{2}x_{2} + 0 \cdot 4 = \frac{7}{2}\\
& \Leftrightarrow & x_{2} = 7
\end{eqnarray*}
$$
\(x_{1}\)を求める
連立一次方程式の1行目に注目します。\(x_{2}, x_{3}\)の値はすでに得られているため、未知変数\(x_{1}\)を求めることができます。
$$
\begin{eqnarray*}
& & 2 x_{1} + 7 -2 \cdot 4 & = & 1\\
& \Leftrightarrow & 2 x_{1} = 1 - 7 + 8\\
& \Leftrightarrow & x_{1} = 1
\end{eqnarray*}
$$
疑似コード
考え方では、\(3 \times 3\)行列で考えましたが、一般化すると以下のようなアルゴリズムになります。
Input A, b
# A: a[0][0] ~ a[n - 1][n - 1]
# b: b[0] ~ b[n - 1]
# 前進消去
for k = 0 To n - 1
for i = k + 1 To n - 1
w ← a[i][k] / a[k][k]
for j = k To n - 1
a[i][j] ← a[i][j] - a[k][j] * w
end for
end for
end for
# 後退代入
for i = n - 1 To 0
sum ← b[i]
for j = i + 1 To n
sum ← sum - a[i][j] * b[j]
end for
b[i] ← sum / a[i][i]
end for
Output b
C言語にする実装
実装結果は、GitHubにあります。必要に応じて参照してください。
https://github.com/yuruto-free/gaussian-elimination
gaussian_elimination.h
#ifndef GAUSSIAN_ELIMINATION_H__
#define GAUSSIAN_ELIMINATION_H__
#include
#define GE_OK (0)
#define GE_NG (1)
/**
* \brief ガウスの消去法
* \param[in] dim 次元数
* \param[in] matrix 係数行列
* サイズ:dim * dim
* \param[inout] vec 右辺ベクトル
* サイズ:dim
* \retval GE_OK 正常終了
* \retval GE_NG 異常終了
*/
int32_t GE_gauss_solver(int32_t dim, double *matrix, double *vec);
#endif
gaussian_elimination.c
#include "gaussian_elimination.h"
#include
#include
#include
#define RETURN_OK_GE (0)
#define RETURN_NG_GE (1)
#define MEPS_GE (1e-10)
/**
* \brief ピボット選択
* \param[in] dim 次元数
* \param[in] col 対象列
* \param[in] matrix 係数行列
* \param[out] pivot ピボット値
* \retval RETURN_OK_GE 正常終了
* \retval RETURN_NG_GE 異常終了
*/
static int32_t pivot_selection(int32_t dim, int32_t col, double *matrix,
int32_t *pivot);
/**
* \brief 入れ替え処理
* \param[in] dim 次元数
* \param[in] row 入れ替え元の行
* \param[in] pivot 入れ替え先の行
* \param[inout] matrix 係数行列
* \param[inout] vec 右辺ベクトル
* \retval RETURN_OK_GE 正常終了
* \retval RETURN_NG_GE 異常終了
*/
static int32_t swap(int32_t dim, int32_t row, int32_t pivot, double *matrix,
double *vec);
int32_t GE_gauss_solver(int32_t dim, double *matrix, double *vec) {
int32_t ret = (int32_t)GE_NG;
int32_t func_val;
int32_t pivot, idx;
int32_t row, col;
double diag, scale, sum;
if ((NULL != matrix) && (NULL != vec)) {
/* 前進消去 */
for (idx = 0; idx < dim - 1; idx++) {
/* ピボット選択 */
func_val = pivot_selection(dim, idx, matrix, &pivot);
if ((int32_t)RETURN_OK_GE != func_val) {
goto EXIT_GE_SOLVER;
}
/* 対角成分の絶対値が最大でない場合、行を入れ替える */
if (idx != pivot) {
func_val = swap(dim, idx, pivot, matrix, vec);
if ((int32_t)RETURN_OK_GE != func_val) {
goto EXIT_GE_SOLVER;
}
}
/* 対角成分の最大値を取得 */
diag = matrix[idx * dim + idx];
if (fabs(diag) < (double)MEPS_GE) {
goto EXIT_GE_SOLVER;
}
for (row = idx + 1; row < dim; row++) {
scale = matrix[row * dim + idx] / diag;
for (col = idx; col < dim; col++) {
matrix[row * dim + col] -= matrix[idx * dim + col] * scale;
}
vec[row] -= vec[idx] * scale;
}
}
/* 後退代入 */
for (row = dim - 1; row >= 0; row--) {
sum = vec[row];
for (col = row + 1; col < dim; col++) {
sum -= matrix[row * dim + col] * vec[col];
}
vec[row] = sum / matrix[row * dim + row];
}
ret = (int32_t)GE_OK;
}
EXIT_GE_SOLVER:
return ret;
}
static int32_t pivot_selection(int32_t dim, int32_t col, double *matrix,
int32_t *pivot) {
int32_t ret = (int32_t)RETURN_NG_GE;
int32_t i;
int32_t p;
double max_val, val;
if ((NULL != matrix) && (NULL != pivot)) {
/* 対角成分の絶対値が最大値となると仮定 */
max_val = fabs(matrix[col * dim + col]);
p = col;
/* 対角成分より下側の値のうち、絶対が最大となるものを探索 */
for (i = col + 1; i < dim; i++) {
val = fabs(matrix[i * dim + col]);
if (max_val < val) {
max_val = val;
p = i;
}
}
(*pivot) = p;
ret = (int32_t)RETURN_OK_GE;
}
return ret;
}
static int32_t swap(int32_t dim, int32_t row, int32_t pivot, double *matrix,
double *vec) {
int32_t ret = (int32_t)RETURN_NG_GE;
int32_t col;
double tmp;
if ((NULL != matrix) && (NULL != vec)) {
/* 行の入れ替え */
for (col = 0; col < dim; col++) {
tmp = matrix[row * dim + col];
matrix[row * dim + col] = matrix[pivot * dim + col];
matrix[pivot * dim + col] = tmp;
}
/* 右辺ベクトルの入れ替え */
tmp = vec[row];
vec[row] = vec[pivot];
vec[pivot] = tmp;
ret = (int32_t)RETURN_OK_GE;
}
return ret;
}
ここで、部分ピボット選択という処理を追加しています。上記で解説していなかったので、この場で紹介します。
部分ピボット選択
ここでは、以下のような連立一次方程式を考えます。
$$
\begin{eqnarray*}
\left(
\begin{array}{ccc}
0 & 0 & 2\\
3 & 3 & 1\\
-2 & 2 & 0\\
\end{array}
\right) \left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
1\\
2\\
3\\
\end{array}
\right)
\end{eqnarray*}
$$
上記の連立一次方程式の場合、\((1, 1)\)成分が0となっているため、前進消去ができないです。これを解決するには、対象としている列(ここでは、1列目)のうち、対角成分以降で絶対値が最大となるものと入れ替えれば良いです。
ここでは、2行目が最大となるので、1行目と2行目を入れ替えると以下のようになります。
$$
\begin{eqnarray*}
\left(
\begin{array}{ccc}
3 & 3 & 1\\
0 & 0 & 2\\
-2 & 2 & 0\\
\end{array}
\right) \left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
2\\
1\\
3\\
\end{array}
\right)
\end{eqnarray*}
$$
1列目に対し、前進消去を行うと、以下のようになります。
$$
\begin{eqnarray*}
\left(
\begin{array}{ccc}
3 & 3 & 1\\
0 & 0 & 2\\
0 & 4 & \frac{2}{3}\\
\end{array}
\right) \left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
2\\
1\\
\frac{13}{3} \\
\end{array}
\right)
\end{eqnarray*}
$$
同様に、\((2, 2)\)成分が0となっているため、行を入れ替える必要があります。ここでは、2行目と3行目を入れ替えれば良いです。入れ替えた結果は以下のようになります。
$$
\begin{eqnarray*}
\left(
\begin{array}{ccc}
3 & 3 & 1\\
0 & 4 & \frac{2}{3}\\
0 & 0 & 2\\
\end{array}
\right) \left(
\begin{array}{c}
x_{1}\\
x_{2}\\
x_{3}\\
\end{array}
\right) = \left(
\begin{array}{c}
2\\
\frac{13}{3} \\
1\\
\end{array}
\right)
\end{eqnarray*}
$$