学习笔记——高斯消元
简介
高斯消元是一个用于求方程组的解的算法。在线性代数中非常重要。一般而言,其复杂度为,可以承受及以下的数据范围(当然有的题目时限是什么的,特殊情况特殊对待)。
算法理解
考虑一个二元一次方程组怎么解。
首先将第一个方程化为,然后两边再同乘,这样在两个方程中,的系数就都化为,然后两式相减,就可以消去,然后就可以解出,再回代,求解。
那么多元方程组也是一样的思路。
首先把第一个方程化为,然后对于之后的每一个方程,都把第一个方程乘上当前这个方程的的系数,然后相减,就可以得到个不含的方程,然后再对这剩下的个方程,重复上述操作即可。最后先求解出,然后不停地代到上一个方程里去解,最终得到整个方程组的解。
那么在代码实现的时候,是先把每个未知数的系数拎出来,形成一个矩阵(一般是)。
为了代码美观方便起见,下标从0开始。
美其名曰:增广矩阵。注意,最后一列是等号右边的常数项。
算法实现
枚举当前要消的元,在增广矩阵中就是枚举第几列。
选取矩阵中的一行,将含这个元的系数化为。即
特别地,为了减小精度损失和代码方便,一般是找系数最大的一行。而如果系数最大的系数也是,那么说明当前这个元的系数不用消,则直接进入下一个消元,并对此元进行标记(至于标记什么后面会提到)。
对于后面的每一行,都将第的(刚刚已经化为)乘上第行第的系数。然后两行相减,即可以将行的第这一元消去。
枚举完后,增广矩阵应该是变成了上三角矩阵,即
有重要性质:一个位置为,则其左下所有位置都为。
然后求解
先判断是否有解。
那么如果无解,说明有某一次消元时得到了的情况。所以在最后的增广矩阵中找未知数系数全为但是等号右边不为的行,即的行,则无解。
在代码中,运用上面增广矩阵的性质,可以从最后的开始直接遍历最后一个常数项看是否有不等于的,则其左边的所有系数一定都为,此时可以判断无解。
如果多解,那么说明消元的时候出现了的情况。所以在增广矩阵中找全行,如果有,则说明有多个解。
注意:对于无解或多解情况,增广矩阵还是遵照类三角矩阵,因此,如果按照上面所述的方式进行消元,那么最后比方程个数小,就会多解,而之前标记的就是可以取任意数的元,我们称之为自由元,一般而言,最后自由元个数,其中为方程个数。
如果有唯一解,那么就回代求解。此时增广矩阵一定是严格的上三角矩阵,先求出最后一个元,然后将最后一个元代入倒数第二个消元后的方程中,一定可以化为,就可以依次求出每一个元,从而解得方程。
几种常见方程类型
算法放在函数中,你需要先将方程组构造在中,然后调用:
int res=Gauss(equ,var);
if(res==-1) 无解
//else if(res==-2) 浮点解,整数线性方程组中用
else if(res>0) 多解,此时res为自由元个数
else 有唯一解,输出x[]即可
浮点线性方程组#
此种题目要特别注意精度问题,一般取。
这种方程组一般放在电学题目中,此时运用基尔霍夫电流定律,对于每个节点列出方程。
double a[MAXN][MAXN],x[MAXN];//a:增广矩阵 x:解
bool frex[MAXN];//标记是否为自由元
int Gauss(int equ,int var){//equ:方程个数 var:未知数个数
for(int i=0;i<=var;i++) x[i]=0,frex[i]=1;//初始化
//step1 消元
int col=0,row;//枚举第row行,第col列,即第row个方程,消去第col个元
for(row=0;row<equ&&col<var;row++,col++){
//step1.1
int mx_r=row;//找出系数最大的一行,减小精度,便于判断
for(int i=row+1;i<equ;i++)
if(abs(a[i][col])>abs(a[mx_r][col]))
mx_r=i;
if(mx_r!=row)
for(int j=col;j<=var;j++)
swap(a[row][j],a[mx_r][j]);//把最大的那一行与当前处理的交换
if(fabs(a[row][col])<eps){row--;continue;}//如果最大的一行都是0,那么该元已经是0,跳过
//step1.2
for(int i=row+1;i<equ;i++)
if(fabs(a[i][col])>eps){
double tmp=a[i][col]/a[row][col];//把当前方程的col元系数化为1
for(int j=col;j<=var;j++)
a[i][j]-=a[row][j]*tmp;//乘上col元的系数,消元
a[i][col]=0;//避免精度影响,当前方程该元赋为0
}
}
//step2
for(int i=row;i<equ;i++)
if(fabs(a[i][col])>eps)
return -1;//step2.1
if(var>row) return var-row;//step2.2
//step2.3
for(int i=var-1;i>=0;i--){
double tmp=a[i][var];
for(int j=i+1;j<var;j++)
tmp-=x[j]*a[i][j];
x[i]=tmp/a[i][i];//回代求解
}return 0;
}
例题
模板题
Electric resistance
计算电压
异或方程组#
此种题目是形如
众所周知,异或相当于模二,所以消元的时候就是系数之间异或就可以了。
一般是关联开关问题,对于每个开关,比较前后状态和相邻的开关,可以得出方程。
int a[MAXN][MAXN],x[MAXN],frX[MAXN];//记录自由元
int Gauss(int equ,int var){
for(int i=0;i<=var;i++)
x[i]=frX[i]=0;
//step1
int col=0,fnum=0,row;
for(row=0;row<equ&&col<var;row++,col++){
//step1.1
int mx_r=row;
for(int i=row+1;i<equ;i++)
if(abs(a[i][col])>abs(a[mx_r][col]))
mx_r=i;
if(mx_r!=row)
for(int j=row;j<=var;j++)
swap(a[row][j],a[mx_r][j]);
if(a[row][col]==0){
frX[fnum++]=col;//如果当前的元不用消,说明是自由元
row--;continue;
}
//step1.2
for(int i=row+1;i<equ;i++)
if(a[i][col]!=0)
for(int j=col;j<=var;j++)
a[i][j]^=a[row][j];
}
//step2
for(int i=row;i<equ;i++)
if(a[i][col]!=0)
return -1;//step2.1
int temp=var-row;
if(row<var) return temp;//step2.2
//step2.3
for(int i=var-1;i>=0;i--){
x[i]=a[i][var];
for(int j=i+1;j<var;j++)
x[i]^=(a[i][j]&&x[j]);
}return 0;
}
特别地,对于异或方程组,还有对自由元的处理,可能出现问你有多少自由元,或者解为的最少个数等等。其中问的最少个数,只能枚举算一下,目前没有更好的办法。
时,注意上面的板子里进行了行交换,所以算的时候不能直接用,要记录一下当前的行,如果是自由元,则行不变,如果不是,则行上升。由于交换后,有效的都在增广矩阵的上部,所以行直接从总方程数减去自由元开始,这样可以使每次都能找到相对应的进行计算。
int ans,var;
void dfs(int row,int i,int sum){
if(sum>=ans) return;//剪枝,很快啊
if(i<0){ans=sum;return;}
if(!frX[i]){
x[i]=a[row][var];
for(int j=i+1;j<var;j++)
x[i]^=(a[row][j]&&x[j]);
dfs(row-1,i-1,sum+x[i]);
}else{
x[i]=0;dfs(row,i-1,sum);
x[i]=1;dfs(row,i-1,sum+1);
}
}
int main()
{
...dfs(equ-freenum-1,var-1,0);
printf("%d",ans);//即最少的1的个数
}
整数线性方程组#
此种题目不允许浮点数解。一般会判断或者输出无整数解等。一般都是裸题。
int GCD(int x,int y){return x%y==0?y:GCD(y,x%y);}
int LCM(int x,int y){return x/GCD(x,y)*y;}
int a[MAXN][MAXN],x[MAXN];
bool frex[MAXN];
int Gauss(int equ,int var){
for(int i=0;i<=var;i++) x[i]=0,frex[i]=1;
//step1
int col=0,row;
for(row=0;row<equ&&col<var;row++,col++){
//step1.1
int mx_r=row;
for(int i=row+1;i<equ;i++)
if(abs(a[i][col])>abs(a[mx_r][col]))
mx_r=i;
if(mx_r!=row)
for(int j=col;j<=var;j++)
swap(a[row][j],a[mx_r][j]);
if(a[row][col]==0){row--;continue;}
//step1.2
for(int i=row+1;i<equ;i++)
if(a[i][col]!=0){
int lcm=LCM(abs(a[i][col]),abs(a[row][col]));
int ta=lcm/abs(a[i][col]),tb=lcm/abs(a[row][col]);//为了避免精度损失,要用lcm
if(a[i][col]*a[row][col]<0) tb=-tb;
for(int j=col;j<=var;j++)
a[i][j]=a[i][j]*ta-a[row][j]*tb;
}
}
//step2
for(int i=row;i<equ;i++)
if(a[i][col]!=0)
return -1;//step2.1
if(var>row) return var-row;//step2.2
//step2.3
for(int i=var-1;i>=0;i--){
int tmp=a[i][var];
for(int j=i+1;j<var;j++)
tmp-=x[j]*a[i][j];
if(tmp%a[i][i]!=0) return -2;//如果无法整除,则无整数解
x[i]=tmp/a[i][i];
}return 0;
}
例题
模板题
暂时没有找到别的。
模线性方程组#
与整数方程组类似,但是当无整数解的时候可以加模数进行更正。比较麻烦,所以放在最后。
int a[MAXN][MAXN],x[MAXN],frX[MAXN];
int GCD(int x,int y){return x%y==0?y:GCD(y,x%y);}
int LCM(int x,int y){return x/GCD(x,y)*y;}
int Gauss(int equ,int var){
for(int i=0;i<=var;i++)
x[i]=0,frX[i]=1;
//step1
int col=0,row;
for(row=0;row<equ&&col<var;row++,col++){
//step1.1
int mx_r=row;
for(int i=row+1;i<equ;i++)
if(abs(a[i][col])>abs(a[mx_r][col]))
mx_r=i;
if(mx_r!=row)
for(int j=row;j<=var;j++)
swap(a[row][j],a[mx_r][j]);
if(a[row][col]==0){row--;continue;}
//step1.2
for(int i=row+1;i<equ;i++)
if(a[i][col]!=0){
int lcm=LCM(abs(a[i][col]),abs(a[row][col]));
int ta=lcm/abs(a[i][col]);
int tb=lcm/abs(a[row][col]);
if(a[i][col]*a[row][col]<0) tb=-tb;
for(int j=col;j<=var;j++)
a[i][j]=((a[i][j]*ta-a[row][j]*tb)%MOD+MOD)%MOD;
}
}
//step2
for(int i=row;i<equ;i++)
if(a[i][col]!=0)
return -1;//step2.1
if(row<var) return var-row;//step2.2
//step2.3
for(int i=var-1;i>=0;i--){
int temp=a[i][var];
for(int j=i+1;j<var;j++){
if(a[i][j]!=0)
temp-=a[i][j]*x[j];
temp=(temp%MOD+MOD)%MOD;
}
while(temp%a[i][i]!=0)
temp+=MOD;//如果不能整除,则不断加模数来更正
x[i]=(temp/a[i][i])%MOD;
}return 0;
}
可能与星期月份等相结合。模数一般较小。
但是也不排除个别毒瘤题目,模数很大,导致加了很久还无法整除,会使得时间复杂度大大上升。因此,可以用扩欧来优化这一过程。
int exGCD(int a,int b,int &x,int &y){
if(b==0){x=1;y=0;return a;}
int c=exGCD(b,a%b,y,x);
y-=a/b*x;return c;
}//扩欧
int solve(int a,int b,int c){
int x,y;
int r=exGCD(a,b,x,y);
x=x*c/r;b/=r;
x=(x%b+b)%b;
return x;
}//解ax+by=c的不定方程,返回x的最小正整数解
const int MAXN=75;
int a[MAXN][MAXN],x[MAXN],frX[MAXN],MOD;
int GCD(int x,int y){return x%y==0?y:GCD(y,x%y);}
int LCM(int x,int y){return x/GCD(x,y)*y;}
int Gauss(int equ,int var){
for(int i=0;i<=var;i++)
x[i]=0,frX[i]=1;
int col=0,row;
for(row=0;row<equ&&col<var;row++,col++){
int mx_r=row;
for(int i=row+1;i<equ;i++)
if(abs(a[i][col])>abs(a[mx_r][col]))
mx_r=i;
if(mx_r!=row)
for(int j=row;j<=var;j++)
swap(a[row][j],a[mx_r][j]);
if(a[row][col]==0){row--;continue;}
for(int i=row+1;i<equ;i++)
if(a[i][col]!=0){
int lcm=LCM(abs(a[i][col]),abs(a[row][col]));
int ta=lcm/abs(a[i][col]);
int tb=lcm/abs(a[row][col]);
if(a[i][col]*a[row][col]<0) tb=-tb;
for(int j=col;j<=var;j++)
a[i][j]=((a[i][j]*ta-a[row][j]*tb)%MOD+MOD)%MOD;
}
}
for(int i=row;i<equ;i++)
if(a[i][col]!=0)
return -1;
if(row<var) return 1;
for(int i=var-1;i>=0;i--){
int temp=a[i][var];
for(int j=i+1;j<var;j++){
if(a[i][j]!=0)
temp-=a[i][j]*x[j];
temp=(temp%MOD+MOD)%MOD;
}
temp+=MOD*solve(MOD,a[i][i],-temp);//直接扩欧得出
//其他部分和暴力枚举是一样的
x[i]=(temp/a[i][i])%MOD;
}return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?