高斯消元
P3389【模板】高斯消元法。
2023.12.21 更新:更正了小错误,添加了更详细的解释。
前置线性代数知识:线性相关、初等变换、矩阵的秩、阶梯矩阵。
思路
这道题目是一个很好的考察队线性代数掌握情况的模板题,当然,你用小学数学里学的那个消元法的实质也就是线性方程组的初等变换,而线性方程组的初等变换和矩阵的初等变换在本质上是一样的,所以我觉得用矩阵的初等变换来解释更加直观,因为矩阵的秩可以直接说明方程组解的情况,并且阶梯形矩阵的概念更加直观易懂。
解线性方程组需要消元,消元的操作可以视作对于一个增广矩阵求阶梯矩阵的过程,对于下面这个方程,我们从矩阵的角度来剖析他:
(其中
增广矩阵就可以理解为两个矩阵的组合,而对于一个
把这个矩阵用刚才线性方程组里的字母写出来,就是:
我们要做的第一步实际就是构建这个增广矩阵,构建方法非常简单,直接读入就可以了,因为题目的输入就是按照矩阵的格式读入的,我们在这里假设矩阵为
那么,我们知道,矩阵是可以进行初等变换的,在这里,用采用初等行变换进行消元,初等变换有以下三种:
- 用一非零的数
\lambda 乘以某一整行; - 把一行的
\lambda 倍加到另一行; - 互换任意两行的位置。
(下如需使用,按顺序简称第一种、第二种、第三种)
我们知道,如果我们可以把一个矩阵化成阶梯矩阵的形式,那么就可以直接得到方程的解,所以我们的工作就是通过初等变换,化已知矩阵为阶梯矩阵即可。
我们知道,矩阵的秩就是矩阵中线性无关的行数的极大值(列数也一样,不过显然方程组是横着写的),简记为
可总结为:
- 如果系数矩阵的秩恰等于增广矩阵的秩,且等于未知数数量就是唯一解;
- 如果这个系数矩阵的秩恰不等于增广矩阵的秩就无解,或者增广矩阵的秩大于未知数数量也是无解;
- 剩下的情况就是无数解。
那么,高斯消元就是用已经算好的行去消剩下的行,具体的消除方法如下:
- 如果有解,则一定秩满(
\operatorname{rank}{R}=n ),所以一定满足主对角线上所有元素非零,主对角线以左下所有元素为零,我们从第1 行向第n 行逐步消元,保证\alpha_{i,1}=\dots=\alpha_{i,i-1}=0 且\alpha_{i,i}\neq 0 ,所以我们需要检查第i 行第i 列,如果为零,就需要考虑跟其他第i 列不为0 的没被处理过的行第三种初等变换,如果所有行的第i 列均为0 ,就说明秩不满,直接输出No Solution即可,实质就是扫一遍没处理过的行随便找一个第i 列不为0 的放到第i 行去; - 用第
i 行将第i+1\sim n 行的第i 个元素全部消掉(在进行处理第i 行的时候,前i-1 行肯定都已经消掉了),那么这里需要用到第二种初等变换,具体的操作中,我们需要确定\lambda 的值,很显然,如果我们要将第j (j>i )行的第i 个元素消掉,那么就满足\alpha_{j,i}+\lambda\times\alpha_{i,i}=0 ,显然\lambda=-\frac{\alpha_{j,i}}{\alpha_{i,i}} ;
划为有唯一解的阶梯矩阵以后,矩阵大致形态如下:
有了这样一个矩阵,那么后续就容易了,首先第
写代码时的一些注意点:
- 本题在消元时使用第二种初等变换,不能保证中间结果和最终答案均为整数,需要使用浮点数;
- 注意判断浮点数是否为零的时候需要小心精度差,判
a[i][i]=0的时候请用fabs(a[i][i])<eps而不是a[i][i]==0。否则开了 O2 优化后会因为 FMA 挂掉(详见这篇帖子); - 如果第
i 行第i 列为零,一定要和下面的其他行进行第二种初等变换,否则程序可能输出nan; - 注意
No Solution的拼写方式。
代码里有简要的注释,可供参考。
代码
#include <bits/stdc++.h>
const long double eps=1e-7;
using namespace std;
long double mat[105][105],res[105];
int n;
void trans(int i,int j) {
// 初等变换 3:交换第 i 行和第 j 行
if(i==j) return;
swap(mat[i],mat[j]);
}
void trans(int i,int j,double k) {
if(!i||!j) return;
// 初等变换 2:将第 i 行的 k 倍与第 j 行对应相加
for(int l=1;l<=n+1;++l)
mat[j][l]+=k*mat[i][l];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr),
cout.tie(nullptr);
cin>>n;
for(int i=1;i<=n;++i)
for(int j=1;j<=n+1;++j)
cin>>mat[i][j];
for(int i=1;i<=n;++i) {
bool flag=0;
// 先找到一个第 i 个数字不为 0 的行将其初等变换到第 i 行
for(int j=i;j<=n;++j)
trans(i-1,j,-mat[j][i-1]/mat[i-1][i-1]);
for(int j=i;j<=n;++j)
if(!fabs(mat[j][i])<eps) {
trans(i,j);
flag=1;
break;
}
if(!flag) {
// 不满秩,找不到就一定没有唯一解
cout<<"No Solution\n";
return 0;
}
if(!mat[i][i]) {
// 初等变换把 (i,i) 也变成 0 了
cout<<"No Solution\n";
return 0;
}
// 通过初等变换 2 进行消元
} // 变为阶梯形矩阵后,从下往上求方程的解
for(int i=n;i>=1;--i) {
res[i]=mat[i][n+1];
for(int j=n;j>i;--j)
res[i]-=mat[i][j]*res[j];
res[i]/=mat[i][i];
// 用之前算出的结果从下向上代入方程求解
}
for(int i=1;i<=n;++i)
cout<<fixed<<setprecision(2)<<res[i]<<'\n';
// 输出答案
return 0;
}