引入
先来看一个一元一次方程:
3 x − 9 = 0 3x-9=03x−9=0
对于这样一个形如a x + b = 0 ax+b=0ax+b=0的方程,我们很容易的解得x = 3 x=3x=3。
在电脑程序中,我们的处理也很简单:
double solve(double a,double b){ //ax=b
return b/a;
}
我们再看一个二元一次方程组:
{ 2 x + 5 y = 9 ① 4 x − 3 y = 5 ② \left\{ \begin{aligned} 2x+5y=9\qquad ①\\ 4x-3y=5\qquad②\\ \end{aligned} \right.{2x+5y=9①4x−3y=5②
对于人脑来讲,我们可以一眼看出该方程的解为x = 2 , y = 1 x=2,y=1x=2,y=1,但是电脑不行。
为了让这种解法适用于电脑,我们需要还原我们的思考过程。
数学书上教我们的方法是加减消元法:
① × 3 + ② × 5 得 , 26 x = 52 ①\times3+②\times5得,26x=52①×3+②×5得,26x=52
解 得 x = 2 解得x=2解得x=2
将 x = 2 代 入 ② 式 得 8 − 3 y = 5 将x=2代入②式得8-3y=5将x=2代入②式得8−3y=5
解 得 y = 1 解得y=1解得y=1
由上面的过程我们可以看出,加减消元法将一个含有多个未知数的方程组转化成了多个简单的一元一次方程。
将上面的做法扩展到n nn元n nn次方程组,就是我们今天要介绍的高斯消元。
高斯消元
原理
高斯消元的原理基于3 33条定理:
1. 1.1.两方程互换,解不变。
2. 2.2.一方程乘上非零数k kk,解不变。
3. 3.3.一方程加上另一方程,解不变。
实现
我们以这道题为例,
我们的输入是:
1 2 1 7
2 -1 3 7
3 1 2 18
变成常见的方程组:
{ x + 2 y + z = 7 ① 2 x − y + 3 z = 7 ② 3 x + y + 2 z = 18 ③ \left\{ \begin{aligned} x+2y+z=7\qquad ①\\ 2x-y+3z=7\qquad②\\ 3x+y+2z=18\qquad③\\ \end{aligned} \right.⎩⎪⎨⎪⎧x+2y+z=7①2x−y+3z=7②3x+y+2z=18③
我们将其转化成一个n × ( n + 1 ) n\times(n+1)n×(n+1)的矩阵:
[ 1 2 1 ∣ 7 2 − 1 3 ∣ 7 3 1 2 ∣ 18 ] \begin{bmatrix} 1&2&1&|&7 \\2&-1&3&|&7\\ 3&1&2&|&18 \end{bmatrix}\\⎣⎡1232−11132∣∣∣7718⎦⎤
我们再进行除法时会产生精度误差,故我们使x xx最大的在第一行,矩阵变为:
[ 3 1 2 ∣ 18 2 − 1 3 ∣ 7 1 2 1 ∣ 7 ] \begin{bmatrix} 3&1&2&|&18 \\2&-1&3&|&7\\ 1&2&1&|&7 \end{bmatrix}\\⎣⎡3211−12231∣∣∣1877⎦⎤
我们设矩阵的3 33行从上到下分别为R 1 , R 2 , R 3 R_1,R_2,R_3R1,R2,R3,就可以按照常规思路解方程了。
由于方便的考虑,我们需要在消元时把式子里的某一个系数变为1 11。
R 2 × 3 − R 1 × 2 = R 2 × 1.5 − R 1 : R_2\times3-R_1\times2=R_2\times1.5-R_1:R2×3−R1×2=R2×1.5−R1:
[ 3 1 2 ∣ 18 0 − 2.5 2.5 ∣ − 7.5 1 2 1 ∣ 7 ] \begin{bmatrix} 3&1&2&|&18 \\0&-2.5&2.5&|&-7.5\\ 1&2&1&|&7 \end{bmatrix}\\⎣⎡3011−2.5222.51∣∣∣18−7.57⎦⎤
R 3 × 3 − R 1 × 1 : R_3\times3-R_1\times1:R3×3−R1×1:
[ 3 1 2 ∣ 18 0 − 2.5 2.5 ∣ − 7.5 0 5 1 ∣ 3 ] \begin{bmatrix} 3&1&2&|&18 \\0&-2.5&2.5&|&-7.5\\ 0&5&1&|&3 \end{bmatrix}\\⎣⎡3001−2.5522.51∣∣∣18−7.53⎦⎤
R 3 − R 2 × ( − 2 ) = R 3 × ( − 0.5 ) − R 2 : R_3-R_2\times(-2)=R_3\times(-0.5)-R_2:R3−R2×(−2)=R3×(−0.5)−R2:
[ 3 1 2 ∣ 18 0 − 2.5 2.5 ∣ − 7.5 0 0 − 3 ∣ 6 ] \begin{bmatrix} 3&1&2&|&18 \\0&-2.5&2.5&|&-7.5\\ 0&0&-3&|&6 \end{bmatrix}\\⎣⎡3001−2.5022.5−3∣∣∣18−7.56⎦⎤
转化成方程组:
{ 3 x + y + z = 18 − 2.5 y + 2.5 z = − 7.5 − 3 z = 6 \left\{ \begin{aligned} 3x+y+z=18\qquad\\ -2.5y+2.5z=-7.5\\ -3z=6\qquad\qquad\space\space\space\space\space\\ \end{aligned} \right.⎩⎪⎨⎪⎧3x+y+z=18−2.5y+2.5z=−7.5−3z=6
然后解出z zz,由z zz解出y yy,再解出x xx,这个操作被称作回代。
这就是加回代的高斯消元解方程的过程。
细节
消除误差
对于浮点数的运算,误差是一个很常见的事情。
我们假设最后解方程的某系数为0 00,但经过多重运算后,当前的系数变为1 0 − 10 10^{-10}10−10,而我们的程序就会判断此数不为0 00,然后运算出一些奇怪的结果。
对此的解决方案就是定义一个极小值e p s epseps,一般取1 0 − 6 10^{-6}10−6到1 0 − 8 10^{-8}10−8间的数,如果一个数的绝对值小于e p s epseps,那么我们就认为该数为0 00。
#define eps 1e-6
bool check(double x){
if(fabs(x)<=eps) return false;
return true;
}
判断无解或无穷解
先讨论无解的情况。
从数学角度考虑,只有当0 = 非 零 数 0=非零数0=非零数时我们的方程才无解,故如果我们在回代过程中发现消去所有已知量后,某方程的系数全为0 00但常数项非零,那么我们就退出。
int Gauss(){
/*消元+回代*/
if(!check(a[i][i])&&check(a[i][n+1])) return -1;
}
再考虑无穷解的情况。
与上面的情况相似,但当某方程回代后常数项和未知数全为0 00时,方程有无穷解。
int Gauss(){
/*消元+回代*/
if(!check(a[i][i])&&!check(a[i][n+1])) return -1;
}
消元的过程
由于在计算a R n − b R m aR_n-bR_maRn−bRm时我们需要将其中一个系数变为1 11,所以我们让方程组上面的式子的系数大于下面的方程组。
int p=i;
for(int j=1;j<=n;j++)
if(fabs(a[j][i])>fabs(a[p][i])) p=j;
for(int j=1;j<=n+1;j++) swap(a[i][j],a[p][j]);
然后我们进行加减消元。
for(int j=i+1;j<=n;j++){
double tmp=a[j][i]/a[i][i];
for(int k=1;k<=n+1;k++){
a[j][k]=a[j][k]-a[i][k]*tmp;
}
}
回代的过程
将所有已知变量的和减去,判断一下是否有唯一解即可。
for(int i=n;i>=1;i--){
for(int j=n;j>i;j--){
if(a[i][j]!=0) a[i][n+1]-=ans[j]*a[i][j];
}
ans[i]=a[i][n+1]/a[i][i];
}
非回代版高斯消元
优点:精度更好,代码更简单(没有回代的过程)
实现
选择一行的某个未知数为主元,直接消掉其余行的所有带该未知数的项,最后得到的答案矩阵就直接为:
[ a 1 0 0 . . . 0 ∣ A 1 0 a 2 0 . . . 0 ∣ A 2 0 0 a 3 . . . 0 ∣ A 3 . . . . . . . . . . . . . . . ∣ . . . 0 0 0 . . . a n ∣ A n ] \begin{bmatrix} a_1&0&0&...&0&|&A_1\\ 0&a_2&0&...&0&|&A_2\\ 0&0&a_3&...&0&|&A_3\\ ...&...&...&...&...&|&...\\ 0&0&0&...&a_n&|&A_n\\ \end{bmatrix}\\⎣⎢⎢⎢⎢⎡a100...00a20...000a3...0...............000...an∣∣∣∣∣A1A2A3...An⎦⎥⎥⎥⎥⎤
答案x i = A i / a i x_i=A_i/a_ixi=Ai/ai
for(int j=i+1;j<=n;j++){
double tmp=a[j][i]/a[i][i];
for(int k=1;k<=n+1;k++){
a[j][k]=a[j][k]-a[i][k]*tmp;
}
}
而我们实现时的具体差异就是将上方代码的
for(int j=i+1;j<=n;j++)
改为
for(int j=1;j<=n;j++)
又由于当我们处理到某一行时,其前面的一些系数已经被前面几行的操作清零了,所以我们枚举k kk的时候只需从i + 1 i+1i+1枚举到n + 1 n+1n+1就可以了。
for(int k=i+1;k<=n+1;k++){
a[j][k]-=a[i][k]*tmp;
}
Code
#include<bits/stdc++.h>
using namespace std;
double a[105][105];
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n+1;j++){
scanf("%lf",&a[i][j]);
}
}
for(int i=1;i<=n;i++){
int maxn=i;
for(int j=i+1;j<=n;j++){
if(fabs(a[j][i])>fabs(a[maxn][i])){
maxn=j;
}
}
for(int j=1;j<=n+1;j++){
swap(a[i][j],a[maxn][j]);
}
if(!a[i][i]){
cout<<"No Solution\n";
return 0;
}
for(int j=1;j<=n;j++){
if(j==i) continue;
double tmp=a[j][i]/a[i][i];
for(int k=i+1;k<=n+1;k++){
a[j][k]-=a[i][k]*tmp;
}
}
}
for(int i=1;i<=n;i++){
printf("%.2lf\n",a[i][n+1]/a[i][i]);
}
}