ガウスの消去法

この章の目標

ガウスの消去法の前進消去と後退代入により連立一次方程式を解く手順を理解する。部分ピボット選択・完全ピボット選択の仕組み、有理数演算での高さ最小ピボット、LU分解(行交換があれば $PA=LU$)との関係を把握する。

前提知識

  • 行列とベクトルの基本演算
  • 連立一次方程式の基本概念
目次

1. 概要

ガウスの消去法(Gaussian Elimination)は、係数行列 $A$ が正方で正則なとき、連立一次方程式 $A\mathbf{x} = \mathbf{b}$ の一意の解を求める最も基本的な直接法である(特異な場合は、消去の結果から解なし・無数の解を判定できる)。拡大係数行列 $[A|\mathbf{b}]$ に行基本変形を施して係数部分を上三角行列に変換し(前進消去)、その後、後退代入により解を求める。

C. F. Gauss (1777–1855) にちなんで名付けられたが、同様の手法は紀元前の中国の『九章算術』にも見られる。

2. 前進消去

$n \times n$ の係数行列 $A$ と右辺ベクトル $\mathbf{b}$ から拡大係数行列を構成し、第 $k$ 列($k = 1, 2, \ldots, n-1$)について、$k$ 行目より下の全ての行から $k$ 行目の定数倍を引いて $a_{ik}$ を消去する。

消去の乗数

第 $k$ ステップにおいて、ピボット $a_{kk}^{(k)} \neq 0$ のとき(0 なら先に行を交換する。§5)、$i > k$ に対し乗数 $m_{ik}$ を

$$m_{ik} = \dfrac{a_{ik}^{(k)}}{a_{kk}^{(k)}}$$

と定め、第 $i$ 行から第 $k$ 行の $m_{ik}$ 倍を引く:$a_{ij}^{(k+1)} = a_{ij}^{(k)} - m_{ik}\, a_{kj}^{(k)}$。右辺も同じ行操作で $b_i^{(k+1)} = b_i^{(k)} - m_{ik}\, b_k^{(k)}$ と更新する。

拡大係数行列 [A | b]
1 / 1
図1. 前進消去のアニメーション。各ステップでピボット(青)の下の要素を、乗数 $m$ を用いた行操作で 0 にし、最終的に上三角行列を得る。後退代入の様子は次節の図2を参照。

3. 後退代入

前進消去により、対角成分 $u_{kk}$ がすべて 0 でない上三角形 $U\mathbf{x} = \mathbf{c}$ が得られたら、最後の行から順に解を求める:

$$x_n = \dfrac{c_n}{u_{nn}}, \qquad x_k = \dfrac{1}{u_{kk}}\!\left(c_k - \displaystyle\sum_{j=k+1}^{n} u_{kj}\, x_j\right), \quad k = n{-}1, \ldots, 1$$
上三角行列 [U | c]
1 / 1
図2. 後退代入のアニメーション。図1の前進消去で得た上三角行列 $[U\,|\,c]$ から、最下行の式($-6x_5 = 12$)を解いて $x_5$ を求め、それを上の行へ代入しながら $x_4, x_3, x_2, x_1$ を順に決定する。各行では対角成分(青)で割り、既知の解の係数(緑)と右辺の定数(橙)を用いる。

4. 計算過程のアニメーション例

図1・図2(5 元)とは別に、ここではより小さな3変数の連立方程式を例に、前進消去と後退代入の全過程をステップごとに確認する。

ガウスの消去法(3変数)
1 / 9

拡大係数行列による計算例

上記のアニメーションと同じ連立方程式を、拡大係数行列の行操作として表記する。

前進消去:

$$\left[\begin{array}{ccc|c} 2 & 1 & -1 & 8 \\ -3 & -1 & 2 & -11 \\ -2 & 1 & 2 & -3 \end{array}\right]$$

$R_2 \leftarrow R_2 + \dfrac{3}{2}R_1$, $R_3 \leftarrow R_3 + R_1$:

$$\left[\begin{array}{ccc|c} 2 & 1 & -1 & 8 \\ 0 & \dfrac{1}{2} & \dfrac{1}{2} & 1 \\ 0 & 2 & 1 & 5 \end{array}\right]$$

$R_3 \leftarrow R_3 - 4R_2$:

$$\left[\begin{array}{ccc|c} 2 & 1 & -1 & 8 \\ 0 & \dfrac{1}{2} & \dfrac{1}{2} & 1 \\ 0 & 0 & -1 & 1 \end{array}\right]$$

後退代入: 3行目から $x_3 = -1$、2行目に代入して $x_2 = 3$、1行目に代入して $x_1 = 2$。

部分ピボット選択付き C++ 実装例

#include <vector>
#include <cmath>
#include <stdexcept>
#include <limits>
#include <algorithm>

std::vector<double> gaussianElimination(
    std::vector<std::vector<double>> A,
    std::vector<double> b)
{
    const int n = static_cast<int>(A.size());
    // 特異判定の閾値: 行列の最大要素に比例させる(スケールに左右されない)
    double scale = 0.0;
    for (const auto& row : A) for (double v : row) scale = std::max(scale, std::abs(v));
    const double tol = 100 * std::numeric_limits<double>::epsilon() * scale;
    for (int k = 0; k < n - 1; ++k) {
        // 部分ピボット選択: k 列の絶対値最大行を探す
        int pivot = k;
        for (int i = k + 1; i < n; ++i)
            if (std::abs(A[i][k]) > std::abs(A[pivot][k])) pivot = i;
        if (pivot != k) { std::swap(A[k], A[pivot]); std::swap(b[k], b[pivot]); }
        if (std::abs(A[k][k]) <= tol)
            throw std::runtime_error("singular matrix");
        // 消去
        for (int i = k + 1; i < n; ++i) {
            double m = A[i][k] / A[k][k];
            for (int j = k; j < n; ++j) A[i][j] -= m * A[k][j];
            b[i] -= m * b[k];
        }
    }
    if (std::abs(A[n - 1][n - 1]) <= tol)   // 最後のピボットも検査する
        throw std::runtime_error("singular matrix");
    // 後退代入
    std::vector<double> x(n);
    for (int i = n - 1; i >= 0; --i) {
        double s = b[i];
        for (int j = i + 1; j < n; ++j) s -= A[i][j] * x[j];
        x[i] = s / A[i][i];
    }
    return x;
}

実装上のアドバイス

  • 行交換のコスト: 大規模行列では行ベクトル全体の物理コピーが高価。代わりに行インデックスの順列 $\pi$ を保持して間接参照する(A[π[i]][j])方式が標準。
  • 特異判定の閾値: $|a_{kk}^{(k)}| \le \tau \cdot \max_{i,j} |a_{ij}|$($\tau$ は計算機イプシロンの数十〜100 倍程度。上の実装例は $\tau = 100\varepsilon$)で特異とみなす。$10^{-12}$ のような絶対値の閾値だけで判断すると行列のスケールに左右される。
  • ブロック化: 性能が重要な場合、ブロック分割(LAPACK の dgetrf)と BLAS-3 への委譲でキャッシュ効率を稼ぐ。教科書的な要素単位ループは小規模向け。

5. ピボット選択

前進消去の第 $k$ ステップにおいて、ピボット $a_{kk}^{(k)}$ がゼロまたはゼロに近い場合、ゼロ除算や大きな丸め誤差が生じる。これを避けるためにピボット選択を行う。

5.1 部分ピボット選択(Partial Pivoting)

第 $k$ 列の $k$ 行目以下で絶対値が最大の要素を選び、行の交換を行う。行の交換は連立方程式の式の順番を入れ替えるだけなので、変数の対応関係は変わらない。実用上はほとんどの場合これで十分であり、LAPACKなどの標準的な数値計算ライブラリでも採用されている。

* * * * 0 2 5 3 0 7 1 4 0 3 6 2 探索列 行交換 第2列の k=2 以下で |7| が最大 → 第3行をピボット行に
図3. 部分ピボット選択の概念図。第 $k$ 列で絶対値最大の要素がある行とピボット行を交換する。

5.2 完全ピボット選択(Complete Pivoting)

完全ピボット選択では、第 $k$ ステップにおいて 係数行列の $k$ 行目以下かつ $k$ 列目から右の全要素(右辺の列は含めない)から絶対値最大のものを探し、行と列の両方を交換する。

* * * * 0 2 5 3 0 1 9 4 0 3 6 2 探索範囲(k=2 行目以下・k=2 列目から右) 行交換 列交換 探索範囲の全要素から |9| が最大 → 第3行と第2行を交換 → 第3列と第2列を交換 (= x₂ と x₃ の入れ替え)
図4. 完全ピボット選択の概念図。探索範囲(黄色)の全要素から絶対値最大を選び、行交換(赤)と列交換(青)の両方を行う。

列の交換は変数の順序の入れ替えに相当する。例えば第2列と第3列を交換すると、$x_2$ と $x_3$ の役割が入れ替わる。したがって、全ての消去が終わった後に列の交換履歴を逆順にたどって、変数の順序を元に戻す必要がある。

部分 vs 完全:完全ピボット選択は数値的安定性がわずかに優れるが、最大要素の探索が各ステップで $O(n^2)$、全体で $O(n^3)$(消去そのものと同じ次数)かかる。部分ピボット選択の探索は各ステップで $O(n)$、全体で $O(n^2)$ であり、実用上は部分ピボット選択で十分な場合がほとんどである。

5.3 有理数演算でのピボット選択

上記のピボット選択戦略は浮動小数点演算を前提としている。有理数(多倍長整数による分数)で厳密にガウスの消去法を実行する場合、丸め誤差は発生しないが、代わりに中間値の膨張(coefficient swell)が問題となる。消去の過程で分子・分母の桁数が爆発的に増大し、計算が極端に遅くなることがある。

この問題を軽減するために、浮動小数点とは違って値の大きさ(絶対値)ではなく、分子・分母の大きさに注目してピボットを選ぶ方法がある。有理数 $p/q$(既約分数)の高さ $H(p/q) = \max(|p|, |q|)$ が小さい(ゼロでない)要素をピボットに選ぶと、そのステップの乗数の分子・分母が小さくなりやすい。ただしこれは局所的な目安(ヒューリスティック)であり、全体の中間値の膨張を最小にする保証はない。例えば $1/1000$ は絶対値こそ小さいが高さは $1000$、$2/3$ は絶対値が大きいが高さは $3$ である。高さ最小ピボットでは(絶対値が大きくても高さの小さい)$2/3$ を選ぶ点が、浮動小数点での絶対値最大ピボットとの違いである。

補足:有理数のガウスの消去法では、ピボット選択以外にも、各ステップで中間値を既約分数に約分する、あるいはBareissのアルゴリズム(分数を使わず整数のまま消去する手法)を用いるなど、中間値膨張を避ける工夫がある。

6. 計算量

$n$ 元連立一次方程式に対するガウスの消去法の計算量は次の通りである。

  • 前進消去: $\dfrac{2}{3}n^3 + O(n^2)$ 回の浮動小数点演算
  • 後退代入: $n^2 + O(n)$ 回の浮動小数点演算
  • 全体: $O(n^3)$

7. LU分解との関係

行交換をせずに消去できる場合($A$ の先頭主小行列式がすべて 0 でない場合)、ガウスの消去法の前進消去は、行列 $A$ を下三角行列 $L$ と上三角行列 $U$ の積に分解する操作と等価である:

$$A = LU$$

消去の乗数 $m_{ik}$ が $L$ の対角より下の要素となり、消去後の上三角行列が $U$ である。部分ピボット選択で行を交換する場合は、置換行列 $P$ を用いて $PA = LU$、完全ピボット選択で列も交換する場合は $PAQ = LU$ と表す(行を交換するときは、それまでの乗数も同じ行へ並べ替える)。一度 $LU$ 分解を行えば、異なる右辺 $\mathbf{b}$ に対して $O(n^2)$ で解が求まるため、同じ係数行列で複数の右辺を解く場合に効率的である。

8. 注意点

  • 要素成長率と数値安定性: 部分ピボット選択付きガウスの消去法の数値安定性は、消去過程での要素成長率 $\rho_n = \max_{i,j,k} |a_{ij}^{(k)}| / \max_{i,j} |a_{ij}|$ に左右される。理論最悪値は $\rho_n \le 2^{n-1}$ で Wilkinson 行列により達成されるが、多くの実用例やランダム行列では経験的に小さく($n$ 程度以下に)収まることが多い(保証ではない)。完全ピボットでは $\rho_n \le n^{1/2} (2 \cdot 3^{1/2} \cdots n^{1/(n-1)})^{1/2}$ と緩やかに増大する。
  • スケーリング感受性: 行のスケールが極端に異なる行列では、絶対値最大という基準だけでは不適切なピボットを選ぶことがある。各行を $\|A_{i,:}\|_\infty$ で正規化してから判定する「スケール付き部分ピボット選択」を採用する実装もある。
  • 特異・準特異の検出: $|a_{kk}^{(k)}| \le \tau \cdot \max_{i,j} |a_{ij}|$($\tau$ は上の実装上のアドバイスと同じく計算機イプシロンの数十〜100 倍程度。行列の次元・スケーリング・必要な精度に応じて選ぶ目安)になったら、行列は数値的に特異とみなし、最小二乗法や SVD など別アルゴリズムに切替えるのが安全である。
  • 条件数による精度限界: 計算された解 $\hat{\mathbf{x}}$ の相対誤差は概ね $\kappa(A) \cdot u$($u$ は丸めの単位)のオーダーであり、$\kappa(A) \approx 1/u$ になると有効桁が完全に失われる。事前に条件数推定(LAPACK の ?gecon 等)を行うのが望ましい。
  • 反復改良 (iterative refinement): 残差 $\mathbf{r} = \mathbf{b} - A\hat{\mathbf{x}}$ を高精度で計算し、$A\mathbf{d} = \mathbf{r}$ を $LU$ 分解で解いて $\hat{\mathbf{x}} \leftarrow \hat{\mathbf{x}} + \mathbf{d}$ を 1〜2 回反復すれば、$\kappa(A) \cdot u \ll 1$ の下で精度を取り戻せる。
  • 密行列前提: ガウスの消去法は密行列向けで、疎行列に直接適用するとフィルイン(消去で 0 が非 0 に変わる現象)により疎性が失われる。

条件数と精度の具体例: ヒルベルト行列

$H_{ij} = 1/(i+j-1)$ で定義されるヒルベルト行列は典型的な悪条件行列で、$n = 10$ で $\kappa_2(H_{10}) \approx 1.6 \times 10^{13}$ に達する。倍精度($u \approx 1.1 \times 10^{-16}$)では

$$\kappa_2(H_{10}) \cdot u \approx 1.8 \times 10^{-3}$$

つまり相対誤差は $10^{-3}$ 程度まで増幅されうるので、有効数字 3 桁程度しか期待できない。$n = 15$ では $\kappa_2(H_{15}) \approx 6 \times 10^{20}$ となり(倍精度で条件数を計算すると、丸めのため $10^{17}$ 程度に見える)、倍精度では実質的に意味のある解は出ない。対策は (i) 拡張精度(多倍長浮動小数点や四倍精度)への切替、(ii) 反復改良、(iii) 直交化を用いる QR / SVD 解法への切替、のいずれか。

疎行列とスパース LU 分解

疎行列では、消去の順序が悪いとフィルインが連鎖し、疎性が大きく失われる(例:先頭の行と列だけが密な「矢じり形」の行列を素朴に消去すると、上三角がほぼすべて埋まる)。一方、帯行列ではフィルインは帯の中に収まり(部分ピボット選択をしても上側の帯幅は高々もとの上下の帯幅の和)、帯行列専用の解法(LAPACK の ?gbsv など)が使える。一般の疎行列でフィルインを抑えるため、スパース $LU$ ソルバは次の 3 段階で動く:

  1. 順序付け: 行・列の並べ替えでフィルインを少なくする(最小にする並べ方を求めるのは一般に難しいので、近似的な手法を使う)。Minimum Degree、AMD (Approximate Minimum Degree)、Nested Dissection など。
  2. 記号分解: 数値計算前に、フィルインで非 0 になる位置のみを確定し、データ構造を確保する。
  3. 数値分解: 確定されたパターンに沿って実際の $LU$ 値を計算する。

代表的な実装: SuperLU(汎用)、UMFPACK(非対称。SciPy の spsolve は scikit-umfpack を入れたときに使い、既定では SuperLU を使う)、PARDISO(Intel MKL)、MUMPS(並列)。

9. よくある質問

Q1. ガウスの消去法とは何ですか?

係数行列が正方で正則な連立一次方程式を、前進消去と後退代入の2段階で解く直接法である。計算量は $O(n^3)$ であり、数値線形代数の最も基本的なアルゴリズムである。

Q2. ピボット選択はなぜ必要ですか?

ピボット要素が 0 またはゼロに近いとゼロ除算や大きな丸め誤差が発生する。部分ピボット選択により列内の絶対値最大の要素をピボットに選ぶことで数値的安定性が向上する。

Q3. ガウスの消去法とLU分解の関係は?

行交換をせずに消去できる場合、前進消去の過程は行列 $A$ を $L$(下三角)と $U$(上三角)に分解する操作 $A = LU$ と等価である。消去の乗数が $L$ の要素、消去後の行列が $U$ となる。部分ピボット選択で行を交換する場合は、置換行列 $P$ を用いて $PA = LU$ と表す。

Q4. 部分ピボット選択と完全ピボット選択の違いは?

部分ピボット選択は列内の絶対値最大の要素を探して行を交換する(探索は各ステップ $O(n)$、全体で $O(n^2)$)。完全ピボット選択は係数行列の残りの部分全体から絶対値最大を探し、行と列の両方を交換する(探索は各ステップ $O(n^2)$、全体で $O(n^3)$)。列の交換は変数の順序の入れ替えに相当するため、最後に変数の順序を復元する必要がある。実用上は部分ピボット選択で十分な場合がほとんどである。

Q5. 有理数でガウスの消去法を実行するとき、ピボット選択はどうすべきですか?

有理数(多倍長整数による分数)では丸め誤差がないため、値の大きさではなく分子・分母の大きさが問題になる。高さ $H(p/q)=\max(|p|,|q|)$ の小さい要素をピボットに選ぶと乗数の分子・分母が小さくなりやすいが、全体の中間値の膨張を最小にする保証はない目安である。各ステップでの約分や、分数を使わず整数のまま消去する Bareiss のアルゴリズムも使われる。

10. 参考資料

  • Wikipedia「ガウスの消去法」(日本語版)
  • Wikipedia「Gaussian elimination」(英語版)
  • G. H. Golub & C. F. Van Loan, Matrix Computations, 4th ed., Johns Hopkins, 2013(§3 で $LU$ 分解、§3.4 で要素成長率と Wilkinson 行列)。
  • L. N. Trefethen & D. Bau III, Numerical Linear Algebra, SIAM, 1997(Lecture 20-23 で $LU$ と部分ピボット)。
  • N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002(§9 でガウスの消去法の後退誤差解析、§12 で反復改良)。

sangi での実装

この記事のアルゴリズムは sangi の gaussian_elimination(LinAlg モジュール)で利用できる。