先来看一个一元一次方程:
3 x − 9 = 0 3x-9=0 3x−9=0
对于这样一个形如 a x + b = 0 ax+b=0 ax+b=0的方程,我们很容易的解得 x = 3 x=3 x=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=1 x=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 n n元 n n n次方程组,就是我们今天要介绍的高斯消元。
高斯消元的原理基于 3 3 3条定理:
1. 1. 1.两方程互换,解不变。
2. 2. 2.一方程乘上非零数 k k k,解不变。
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 x x最大的在第一行,矩阵变为:
[ 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 3 3行从上到下分别为 R 1 , R 2 , R 3 R_1,R_2,R_3 R1,R2,R3,就可以按照常规思路解方程了。
由于方便的考虑,我们需要在消元时把式子里的某一个系数变为 1 1 1。
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 z z,由 z z z解出 y y y,再解出 x x x,这个操作被称作回代。
这就是加回代的高斯消元解方程的过程。
对于浮点数的运算,误差是一个很常见的事情。
我们假设最后解方程的某系数为 0 0 0,但经过多重运算后,当前的系数变为 1 0 − 10 10^{-10} 10−10,而我们的程序就会判断此数不为 0 0 0,然后运算出一些奇怪的结果。
对此的解决方案就是定义一个极小值 e p s eps eps,一般取 1 0 − 6 10^{-6} 10−6到 1 0 − 8 10^{-8} 10−8间的数,如果一个数的绝对值小于 e p s eps eps,那么我们就认为该数为 0 0 0。
#define eps 1e-6
bool check(double x){
if(fabs(x)<=eps) return false;
return true;
}
先讨论无解的情况。
从数学角度考虑,只有当 0 = 非 零 数 0=非零数 0=非零数时我们的方程才无解,故如果我们在回代过程中发现消去所有已知量后,某方程的系数全为 0 0 0但常数项非零,那么我们就退出。
int Gauss(){
/*消元+回代*/
if(!check(a[i][i])&&check(a[i][n+1])) return -1;
}
再考虑无穷解的情况。
与上面的情况相似,但当某方程回代后常数项和未知数全为 0 0 0时,方程有无穷解。
int Gauss(){
/*消元+回代*/
if(!check(a[i][i])&&!check(a[i][n+1])) return -1;
}
由于在计算 a R n − b R m aR_n-bR_m aRn−bRm时我们需要将其中一个系数变为 1 1 1,所以我们让方程组上面的式子的系数大于下面的方程组。
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_i xi=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 k k的时候只需从 i + 1 i+1 i+1枚举到 n + 1 n+1 n+1就可以了。
for(int k=i+1;k<=n+1;k++){
a[j][k]-=a[i][k]*tmp;
}
#include
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]);
}
}