高斯消元

缺省源

namespace nazuna_ {
	using namespace std;
#define nzn puts("&&&&&&&")
#define gc p1==p2&&(p2=(p1=buf)+fread(buf,1,iosiz,stdin),p1==p2)?EOF:*p1++
#define iosiz (1<<21)
#define pc putchar
#define ps pc('\n')
#define sp pc(' ')
	char buf[iosiz],*p1=buf,*p2=buf;
	template<typename T> void cmax(T &x,T y){x=x>y?x:y;}
	template<typename T> void cmin(T &x,T y){x=x<y?x:y;}
	template<typename T> T cabs(T x){return x>-x?x:-x;}
	int rd(){
		int x=0,f=1;char c=gc;
		for(;!isdigit(c);c=gc)f=c==45?-1:f;for(;isdigit(c);c=gc)x=x*10+c-48;
		return x=f*x;
	}char ch0[20];short top0;
	template<typename T>void print(T x){
		if(x==0)return pc(48),void();if(x<0)x=-x,pc(45);
		while(x)ch0[top0++]=x%10+'0',x/=10;while(top0)pc(ch0[--top0]);
	}
}

高斯消元

该方法以数学家高斯命名,由拉布扎比.伊丁特改进,发表于法国但最早出现于中国古籍《九章算术》,成书于约公元前150年。(百度百科)

高斯消元是一个求解一般线性方程组的算法,时间复杂度为 \(O(n^3)\),再部分题目的特殊性质下可能不同。

算法流程

原理和我们平时面对二元一次方程组时使用加减消元法相同,但是在程序中我们需要想办法让它以固定的方式求解

我们先考虑一个“弱线性方程”的解法。定义该方程组有 \(n\) 个未知数和 \(n\) 个方程式,形如:

\[\begin{cases} a_{1,1}x_1+a_{1,2}x_2+a_{1,3}x_3+a_{1,4}x_4=b_1\\ a_{2,1}x_1+a_{2,2}x_2+a_{2,3}x_3=b_2\\ a_{3,1}x_1+a_{3,2}x_2=b_3\\ a_{4,1}x_1=b_4\\ \end{cases} \]

这种类似三角形的我们显然一个一个带入消元即可

对于一般的线性方程组,我们考虑把它变成“弱线性方程组”求解。

\[\begin{cases} a_{0,0}x_0+a_{0,1}x_1+a_{0,2}x_2+...+a_{0,{n-1}}x_{n-1}=a_{0,n}\\ a_{1,0}x_0+a_{1,1}x_1+a_{1,2}x_2+...+a_{1,{n-1}}x_{n-1}=a_{1,n}\\ a_{2,0}x_0+a_{2,1}x_1+a_{2,2}x_2+...+a_{2,{n-1}}x_{n-1}=a_{2,n}\\ ...\\ a_{n-1,0}x_0+a_{n-1,1}x_1+a_{n-1,2}x_2+...+a_{n-1,3}x_{n-1}=a_{n-1,n}\\ \end{cases} \]

设我们已经处理了 \(r\) 个方程。选定一个主元 \(x_c(0 \leq c <n)\)

\(a_{i,c}\)\(r \leq i < n\) 中绝对值最大的。

\(a_{i,c}=0\),说明无解或有无穷个解,\(c \gets c+1\) 算下一个。

否则用加减消元消掉 \(a_{j,c}(r<j<n)\)

最后方程就回变成前面的三角形。

如果最后 \(r=n\),说明有解,直接消元。

否则对于\(j \in (r,n)\),若所有\(a_{j,n}\)均等于 \(0\),那就说明有 \((n-r)\) 个自由元,否则报告无解

constexpr double eps=1e-8;
inline int comp(double x,double y){
	if(x-y>eps)return 1;
	if(y-x>eps)return -1;
	return 0;
}
int guass(){
	int r=0;
	ro(c,0,n-1){
		int pos=r;
		ro(i,r+1,n)if(comp(cabs(a[i][c]),cabs(a[pos][c]))==1)pos=i;
		if(comp(a[pos][c],0)==0)continue;
		swap(a[pos],a[r]);
		dw(i,n,c)a[r][i]/=a[r][c];
		ro(i,r+1,n-1){
			if(comp(a[i][c],0)==0)continue;
			dw(j,n,c)a[i][j]-=a[r][j]*a[i][c];
		}
		++r;
	}
	if(r<n){
		ro(i,r,n-1)if(comp(a[i][n],0)!=0)return -1;
		return n-r;
	}
	dw(i,n-2,0)
		ro(j,i+1,n-1)a[i][n]-=a[i][j]*a[j][n];
	return 0;
}

高斯-约旦消元

小优化,相当于只保留对角线的消元方式

在每次消元时直接吧出了当前行 \([0,n)\) 的所有主元消掉,省一点事。

int guass(){
	rep(c,1,n){
		rep(i,c,n)
			if(a[i][c]!=0){
				swap(a[i],a[c]);break;
			}
		if(!a[c][c])return -1;
		int inv=Inv(a[c][c]);
		dep(i,n,c)a[c][i]=a[c][i]*inv%P;
		rep(i,1,n){
			if(i==c||a[i][c]==0)continue;
			dep(j,n,c)a[i][j]-=a[i][c]*a[c][j],a[i][j]=(a[i][j]%P+P)%P;
		}
	}
	return 0;
}

应用

\(1.\)bitset结合珂以 \(O(\frac{n^3}\omega)\) 求解线性异或方程组。

例题:III;

\(2.\) 矩阵求逆。这个我也没搞太懂怎么证明,大概原理应该没问题。

\(I\) 为单位矩阵,因为 \(A^{-1}A=I\)\(A^{-1}I=A^{-1}\),其中 \(A\)\(I\) 进行的线性变换是相同的(因为乘的是同一个矩阵 \(A^{-1}\)),所以如果我们把 \(A \to I\) 的线性变换过程在 \(I\) 上同步进行,那么就可以实现变换 \(I \to A^{-1}\)

我们对矩阵A进行高斯-约旦消元显然可以得到一个只有对角线有值且为 \(1\) 的矩阵,也就是单位矩阵。

为了方便代码实现,我们给出一个增广矩阵,左边是 \(A\),右边是 \(I\),形如:

\[\begin{bmatrix} A_{1,1} &A_{1,2} &A_{1,3} &1 &0 &0 \\ A_{2,1} &A_{2,2} &A_{2,3} &0 &1 &0 \\ A_{3,1} &A_{3,2} &A_{3,3} &0 &0 &1 \end{bmatrix} \]

我们依次把第 \(1\)\(2\)\(3\) 列作为主元消元,使左半部分变成单位矩阵,右半部分就实现 \(I\to A^{-1}\) 了。

例题:V

\(3.\) 求图上随机游走期望。如果是DAG的话直接拓扑排序完DP就行了,但如果不是DAG时有些时候珂以把每个点的期望看成未知数,根据转移关系列方程组,再用高斯消元求解。

例题:VI

例题

I.P3389[模板]高斯消元法

简单板子题

#include<iostream>
#include<cstring>
#define ro(x,l,r) for(int x=l;x<=r;x++)
#define dw(x,r,l) for(int x=r;x>=l;x--)
#define inf 0x3f3f3f3f
#define llinf 0x3f3f3f3f3f3f3f3fll
#define int long long
const int N=105;
const double eps=1e-8;
using namespace nazuna_;
inline int comp(double x,double y){
	if(x-y>eps)return 1;
	if(y-x>eps)return -1;
	return 0;
}
int n;
double a[N][N];
int guass(){
	int r=0,c=0;
	for(;c<n;c++){
		int pos=r;
		ro(i,r+1,n)if(comp(cabs(a[i][c]),cabs(a[pos][c]))==1)pos=i;
		if(comp(a[pos][c],0)==0)continue;
		swap(a[pos],a[r]);
		dw(i,n,c)a[r][i]/=a[r][c];
		ro(i,r+1,n-1){
			if(comp(a[i][c],0)==0)continue;
			dw(j,n,c)a[i][j]-=a[r][j]*a[i][c];
		}
		++r;
	}
	if(r<n){
		ro(i,r,n-1)if(comp(a[i][n],0)!=0)return -1;
		return n-r;
	}
	dw(i,n-2,0)
		ro(j,i+1,n-1)a[i][n]-=a[i][j]*a[j][n];
	return 0;
}
signed main(){
	n=rd();
	ro(i,0,n-1)ro(j,0,n)a[i][j]=rd();
	if(guass()!=0)puts("No Solution");
	else ro(i,0,n-1)printf("%.2lf\n",a[i][n]);
	return 0;
}

II.P2455 [SDOI2006] 线性方程组

真模版,上面那个数据水且没有区分无解和无数解

#include<iostream>
#include<cstring>
#define ro(x,l,r) for(int x=l;x<=r;x++)
#define dw(x,r,l) for(int x=r;x>=l;x--)
#define int long long
constexpr int N=105;constexpr double eps=1e-8;
inline int comp(double x,double y){if(x-y>eps)return 1;if(y-x>eps)return -1;return 0;}
int n;double a[N][N];
int guass(){
	int r=0;
	ro(c,0,n-1){
		int pos=r;
		ro(i,r+1,n)if(comp(cabs(a[i][c]),cabs(a[pos][c]))==1)pos=i;
		if(comp(a[pos][c],0)==0)continue;
		swap(a[pos],a[r]);
		dw(i,n,c)a[r][i]/=a[r][c];
		ro(i,r+1,n-1){
			if(comp(a[i][c],0)==0)continue;
			dw(j,n,c)a[i][j]-=a[r][j]*a[i][c];
		}
		++r;
	}
	if(r<n){
		ro(i,r,n-1)if(comp(a[i][n],0)!=0)return -1;
		return n-r;
	}
	dw(i,n-2,0)
		ro(j,i+1,n-1)a[i][n]-=a[i][j]*a[j][n];
	return 0;
}
signed main(){
	n=rd();ro(i,0,n-1)ro(j,0,n)a[i][j]=rd();
	int cur=guass();
    if(cur==-1)print(-1);
	else if(cur==0)ro(i,0,n-1)printf("x%lld=%.2lf\n",i+1,a[i][n]);
	else print(0);
	return 0;
}

III.P2447 [SDOI2010] 外星千足虫

因为我们只关心千足虫足数的奇偶,我们珂以先假设所有千足虫的脚数都已经模了 \(2\),然后我们发现每次点足就相当于给了一个这样的方程式:

\[a_1x_1 \oplus a_2x_2 \oplus ... \oplus a_nx_n=a_{n+1} \]

把它们放在一起就是一个线性异或方程组。

求线性异或方程组和求解普通的线性方程差不多,先选一个主元,然后保留一个主元是1的方程组,其余全部消掉主元。

这道题还要求最小点多少次,即用到的最靠后的方程组最靠前是多少步,那每次保留主元是 \(1\) 的方程组时贪心取最靠上的就行了,因为你取靠上的一定不会更劣;

注意到 \(N\leq 10^3\),直接 \(O(n^3)\) 消元肯定不行,因为 \(x_i\)\(a_i\) 的取值范围都是 \(\{0,1\}\),所以珂以用bitset优化到\(O(\frac{n^3}\omega)\)

本代码使用了高斯-约旦消元。

#include<iostream>
#include<cstring>
#include<bitset>
#define ro(x,l,r) for(int x=l;x<=r;x++)
#define dw(x,r,l) for(int x=r;x>=l;x--)
#define inf 0x3f3f3f3f
#define llinf 0x3f3f3f3f3f3f3f3fll
#define int long long
const int N=2005;
const double eps=1e-8;
using namespace nazuna_;
int n,m;
bitset<N>a[N];
int guass(){
	int r=0,ans=0;
	ro(c,0,n-1){
		int cur=r;
		ro(i,r,m-1)
			if(a[i][r]){
				cur=i;break;
			}
		if(a[cur][c]==0)return 0;
		cmax(ans,cur+1);
		swap(a[cur],a[r]);
		ro(i,0,m-1)
			if(a[i][c]==1&&i!=r){
				a[i]^=a[r];
			}
		++r;
	}
	return ans;
}
signed main(){
	cin>>n>>m;
	ro(i,0,m-1){
		ro(j,0,n){
			char op;cin>>op;
			a[i][j]=op=='1';
		}
	}
	int ret=guass();
	if(!ret)return puts("Cannot Determine"),0;
	print(ret),ps;
	ro(i,0,n-1)puts(a[i][n]==0?"Earth":"?y7M#");
	return 0;
}

IV.P4035 [JSOI2008] 球形空间产生器

推式子题。把半径也当成一个未知数就是一个 \((n+1)\times (n+1)\) 的方程组了。

V.P4783 【模板】矩阵求逆

矩阵求逆模板。ro变成repdw变成dep

#include<iostream>
#include<cstring>
#define int long long 
constexpr int N=405,P=1e9+7;
using namespace nazuna_;
int a[N][N*2],n;
int qpow(int x,int r){
	int ret=1;
	while(r){
		if(r&1)ret=ret*x%P;
		x=x*x%P,r>>=1;
	}
	return ret;
}
int Inv(int x){return qpow(x,P-2);}
int guass(){
	rep(c,1,n){
		rep(i,c,n)
			if(a[i][c]!=0){
				swap(a[i],a[c]);break;
			}
		if(!a[c][c])return -1;
		int inv=Inv(a[c][c]);
		dep(i,n*2,c)a[c][i]=a[c][i]*inv%P;
		rep(i,1,n){
			if(i==c||a[i][c]==0)continue;
			dep(j,n*2,c)a[i][j]-=a[i][c]*a[c][j],a[i][j]=(a[i][j]%P+P)%P;
		}
	}
	return 0;
}
signed main(){
	n=rd();
	rep(i,1,n){
		rep(j,1,n)a[i][j]=rd();
		a[i][i+n]=1;
	}
	if(guass()==-1)puts("No Solution"),exit(0);
	rep(i,1,n){
		rep(j,1,n)
			print(a[i][j+n]),sp;
		ps;
	}
	return 0;
}

VI.P2973 [USACO10HOL] Driving Out the Piggies G

posted @ 2026-05-31 08:29  Sayhere  阅读(4)  评论(0)    收藏  举报