高斯法求解线性方程组

求解线性方程组的高斯方法(Gauss method for solving system of linear equations)

给出一个有m个未知数的n个线性代数方程组(SLAE, System of Linear Algebraic Equations)。你需要解决的是:确定它是否无解,是否仅有一个解或有无限多的解。如果它至少有一个解,请找出其中任何一个。

a 11 x 1 + a 12 x 2 + ⋯ + a 1 m x m = b 1 a 21 x 1 + a 22 x 2 + ⋯ + a 2 m x m = b 2 ⋮ a n 1 x 1 + a n 2 x 2 + ⋯ + a n m x m = b n \begin{aligned} a_{11} x_1 + a_{12} x_2 + &\dots + a_{1m} x_m = b_1\\ a_{21} x_1 + a_{22} x_2 + &\dots + a_{2m} x_m = b_2\\ &\vdots \\ a_{n1} x_1 + a_{n2} x_2 + &\dots + a_{nm} x_m = b_n \end{aligned} a11x1+a12x2+a21x1+a22x2+an1x1+an2x2++a1mxm=b1+a2mxm=b2+anmxm=bn

其中系数 a i j a_{ij} aij i i i 1 1 1 n n n j j j 1 1 1 m m m )和 b i b_i bi i i i 1 1 1 n n n )是已知的,变量 x i x_i xi i i i 1 1 1 m m m )是未知的。

这个问题也有一个简单的矩阵表示:

A x = b Ax = b Ax=b

其中 A A A 是系数 a i j a_{ij} aij 的大小为 n × m n×m n×m 的矩阵, b b b 是大小为 n n n 的列向量。

值得注意的是,本文介绍的方法也可以用来解以任意数 p p p 为模的方程,即:

a 11 x 1 + a 12 x 2 + ⋯ + a 1 m x m ≡ b 1 ( m o d p ) a 21 x 1 + a 22 x 2 + ⋯ + a 2 m x m ≡ b 2 ( m o d p ) ⋮ a n 1 x 1 + a n 2 x 2 + ⋯ + a n m x m ≡ b n ( m o d p ) \begin{aligned} a_{11} x_1 + a_{12} x_2 + &\dots + a_{1m} x_m \equiv b_1 \pmod p \\ a_{21} x_1 + a_{22} x_2 + &\dots + a_{2m} x_m \equiv b_2 \pmod p \\ &\vdots \\ a_{n1} x_1 + a_{n2} x_2 + &\dots + a_{nm} x_m \equiv b_n \pmod p \end{aligned} a11x1+a12x2+a21x1+a22x2+an1x1+an2x2++a1mxmb1(modp)+a2mxmb2(modp)+anmxmbn(modp)

高斯(Gauss)

严格来说,下面描述的方法应该称为“Gauss-Jordan”,或 Gauss-Jordan 消元法,因为它是 Jordan 在 1887 年描述的 Gauss 方法的一种变体。


概述

该算法是对每个方程中的变量进行 “顺序消除”,直到每个方程只剩下一个变量。如果 n = m n=m n=m ,你可以认为它是将矩阵 A A A 转化为单位矩阵,并在这种明显的情况下解决方程,其解是唯一的且等于系数 b i b_i bi

高斯消元法基于两个简单的变换:

  • 可以交换两个方程
  • 任何方程都可以用该行(具有非零系数)和其他一些行(具有任意系数)的线性组合代替

在第一步, Gauss-Jordan 算法将第一行除以 a 11 a_{11} a11 。然后,该算法将第一行与其余各行相加,使第一列中的系数成为全部为零。为此,在第 i i i 行,我们必须把第一行乘以 − a i 1 -a_{i1} ai1 。注意,这个操作也必须在向量 b b b 上执行。在某种意义上,它的表现就像向量 b b b 是矩阵 A A A 的第 m m m 列一样。

因此,第一步之后,矩阵 A A A 的第一列将由第一行的 1 1 1 和其他行的 0 0 0 组成。

同样地,我们执行算法的第二步,我们考虑第二行的第二列。首先,该行被 a 22 a_{22} a22 除以,然后从其他行中减去,这样所有第二列就变成了 0 0 0 (除了第二行)。

我们对矩阵 A A A 的所有列继续这个过程。如果 n = m n=m n=m ,那么 A A A 将成为单位矩阵。


寻找基准元素(Search for the pivoting element)

所描述的方案遗漏了许多细节。在第 i i i 步,如果 a i i a_{ii} aii 为零,我们不能直接应用描述的方法。相反,我们必须首先 选择一个基准行 :找到矩阵中第 i i i 列非零的一行,然后交换这两行。

请注意,这里我们交换行而不是列。这是因为如果您交换列,那么当您找到解决方案时,您必须记住交换回正确的位置。因此,交换行要容易得多。

在许多实现中,当 a i i ≠ 0 a_{ii} \neq 0 aii=0 时,你可以看到仍然用一些启发式行来交换第 i i i 行,使用一些启发式的方法,如选择绝对值最大的 a j i a_{ji} aji 的启发式行。这种启发式方法是用来在后面的步骤中减少矩阵的取值范围。如果没有这个启发式方法,即使是大小为 20 20 20 的矩阵,误差也会过大,并可能导致 C++ 的浮点数据类型溢出。


降阶案例(Degenerate cases)

m = n m=n m=n 和系统非降阶的情况下(即它有非零行列式,并有唯一的解决方案),上述算法将把 A A A 转化为单位矩阵。

现在我们考虑 一般情况 ,即 n n n m m m 不一定相等,而且方程组是可能降阶的。在这些情况下,第 i i i 步中的基准元素可能找不到。这意味着在第 i i i 列,从当前行开始,全部包含零。在这种情况下,要么变量 x i x_i xi 没有可能的值(意味着 SLAE 没有解决方案),要么 x i x_i xi 是一个独立变量,可以取任意值。在实现 Gauss-Jordan 算法时,你应该继续后续变量的工作,只是跳过第 i i i 列(这相当于删除矩阵的第 i i i 列)。

因此,可以发现过程中的一些变量是独立的。当变量的数量 m m m 大于方程的数量 n n n 时,那么至少可以找到 m − n m-n mn 个独立变量。

一般来说,如果你发现至少有一个自变量,它可以取任何任意的值,而其他(因)变量则通过它来表达。这意味着,当我们在实数领域工作时,方程组可能有无无穷多的解。但你应该记住,当有独立变量时,SLAE可能根本没有解。当剩下的未处理的方程至少有一个非零常数项时,就会发生这种情况。你可以通过给所有独立变量赋零来检查这一点,计算其他变量,然后插入到原来的SLAE中,检查它们是否满足。


实现

以下是 Gauss-Jordan 的实现。选择基准行是通过启发式完成的:在当前列中选择最大值。

函数 Gauss 的输入是矩阵组 a a a ,该矩阵的最后一列是向量 b b b

该函数返回系统的解的数量 ( 0 , 1 , or  ∞ ) (0, 1,\textrm{or } \infty) (0,1,or ) 。如果至少有一个解存在,那么它将被返回到向量 A A A 中。

const double EPS = 1e-9;
const int INF = 2; // it doesn't actually have to be infinity or a big number

int gauss (vector < vector<double> > a, vector<double> & ans) {
	int n = (int) a.size();
	int m = (int) a[0].size() - 1;

	vector<int> where (m, -1);
	for (int col=0, row=0; col<m && row<n; ++col) {
		int sel = row;
		for (int i=row; i<n; ++i)
			if (abs (a[i][col]) > abs (a[sel][col]))
				sel = i;
		if (abs (a[sel][col]) < EPS)
			continue;
		for (int i=col; i<=m; ++i)
			swap (a[sel][i], a[row][i]);
		where[col] = row;

		for (int i=0; i<n; ++i)
			if (i != row) {
				double c = a[i][col] / a[row][col];
				for (int j=col; j<=m; ++j)
					a[i][j] -= a[row][j] * c;
			}
		++row;
	}

	ans.assign (m, 0);
	for (int i=0; i<m; ++i)
		if (where[i] != -1)
			ans[i] = a[where[i]][m] / a[where[i]][i];
	for (int i=0; i<n; ++i) {
		double sum = 0;
		for (int j=0; j<m; ++j)
			sum += ans[j] * a[i][j];
		if (abs (sum - a[i][m]) > EPS)
			return 0;
	}

	for (int i=0; i<m; ++i)
		if (where[i] == -1)
			return INF;
	return 1;
}

实现说明:

  • 该函数使用两个指针——当前列 c o l col col 和当前行 r o w row row
  • 对于每个变量 x i x_i xi ,值 w h e r e ( i ) where(i) where(i) 是该列不为零的行。需要这个向量是因为有些变量可以是独立的。
  • 在这个实现中,当前的第 i i i 行并没有像上面描述的那样除以 a i i a_{ii} aii ,所以最后的矩阵并不是单位矩阵(尽管显然除掉第 i i i 行可以帮助减少错误)。
  • 找到解决方案后,将其重新插入矩阵以检查系统是否至少有一组解。如果测试解决方案成功,则函数返回 1 1 1 i n f inf inf ,取决于是否存在至少一个自变量。

时间复杂度

现在我们应该估计一下这个算法的复杂性。该算法由 m m m 个阶段组成,在每个阶段:

  • 搜索并重新排列基准行。使用上面提到的启发式方法时需要 O ( n + m ) O(n+m) O(n+m)
  • 如果找到当前列中的基准元素,那么我们必须将此等式添加到所有其他等式中,这需要 O ( n m ) O(nm) O(nm) 的时间

因此,算法的最终复杂度为 O ( m i n ( n , m ) ⋅ n m ) O(min(n, m)\cdot nm) O(min(n,m)nm) 。如果 n = m n=m n=m , 复杂度即为 O ( n 3 ) O(n^3) O(n3)

请注意,当SLAE不在实数上,而是在模二中时,那么系统的求解速度就会快得多,这将在下面说明。


算法加速

通过将算法分为两个阶段:正向和反向,之前的实现可以加快2倍。

  • 前进阶段。与之前的实现类似,但当前行只加到它之后的行上。因此,我们得到一个三角矩阵,而不是对角矩阵。
  • 反向阶段。当矩阵为三角矩阵时,我们首先计算出最后一个变量的值。然后将此值插入,找到下一个变量的值。然后插入这两个值以查找下一个变量…

反相只需要 O ( n m ) O(nm) O(nm) ,这比正向阶段快得多。在前向阶段,我们将操作数量减少了一半,从而减少了实现的运行时间。


求解模块化 SLAE

为了解决某些模块中的 SLAE,我们仍然可以使用所描述的算法。但是,如果模块等于 2 2 2 ,我们可以使用按位运算和 C++ 位集数据类型更有效地执行 Gauss-Jordan 消除:

int gauss (vector < bitset<N> > a, int n, int m, bitset<N> & ans) {
    vector<int> where (m, -1);
    for (int col=0, row=0; col<m && row<n; ++col) {
        for (int i=row; i<n; ++i)
            if (a[i][col]) {
                swap (a[i], a[row]);
                break;
            }
        if (! a[row][col])
            continue;
        where[col] = row;

        for (int i=0; i<n; ++i)
            if (i != row && a[i][col])
                a[i] ^= a[row];
        ++row;
    }
        // The rest of implementation is the same as above
}

由于我们使用位压缩,因此实现不仅更短,而且速度提高了 32 32 32 倍。


关于选择基准行的不同启发式方法的一点说明

对于使用什么启发式方法没有一般规则。

先前实现中使用的启发式方法在实践中效果很好。事实证明,它给出的答案几乎与“完全旋转”相同 - 其中旋转行是在其子矩阵的所有元素中搜索(来自当前行和当前列)。

不过,您应该注意,这两种启发式方法都取决于原始方程的缩放程度。例如,如果等式之一乘以 1 0 6 10^6 106 ,那么这个方程几乎肯定会在第一步中被选为基准。这似乎很奇怪,因此更改为更复杂的启发式算法似乎是合乎逻辑的,称为implicit pivoting

隐式旋转比较元素,就好像两条线都被规范化了,所以最大元素是统一的。为了实现这一技术,需要在每一行中保持最大值(或保持每一行以使最大值为一,但这会导致累积误差的增加)。


改进方案

尽管有各种启发式方法, Gauss-Jordan 算法在特殊矩阵中仍会导致大的误差,即使是在大小为 50 − 100 50-100 50100 的矩阵。

因此,有时必须通过应用简单的数值方法来改进得到的 Gauss-Jordan 解——例如,简单迭代的方法。

这样,求解就变成了两步:首先应用 Gauss-Jordan 算法,然后在第一步中采用以初始解为解的数值方法。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我真的不是cjc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值