快乐地打牢基础(9)——高斯消元

高斯消元是一种求解线性方程组的方法。所谓线性方程组,是由MMNN元一次方程共同构成的。线性方程组的所有西数可以写成一个MMNN列的“系数矩阵”,再加上每个方程等号右侧的常数,可以写成一个 MMN+1N + 1列的“增广矩阵”,例如:
{x1+2x2x3=62x1+x23x3=9x1x2+2x3=7[121621391127] \begin{cases} x_1 + 2x_2-x_3=-6\\ 2x_1+x_2-3x_3=-9\\-x_1-x_2+2x_3=7\end{cases}\Longrightarrow \left[ \begin{array}{ccc|c} 1 & 2 & -1 & -6 \\ 2 & 1 & -3 & -9\\ -1 & -1 & 2 & 7 \end{array} \right]

求解方程组的步骤可以概括成对“增广矩阵”的三类操作:
1.用一个非零的数乘某一行;
2.把其中一行的若干倍加到另一行上;
3.交换两行的位置。

这其实就是我们线性代数中矩阵的“初等行变换”。同理,我们也可以定义矩阵的“初等列变换”。用若干次初等行变换求解上面的方程组,过程如下:
[121621391127][121603131127][121603130111] \left[ \begin{matrix} 1 & 2 & -1 & -6 \\ 2 & 1 & -3 & -9\\ -1 & -1 & 2 & 7 \end{matrix} \right] \Longrightarrow \left[ \begin{matrix} 1 & 2 & -1 & -6 \\ 0 & -3 & -1 & 3\\ -1 & -1 & 2 & 7 \end{matrix} \right] \Longrightarrow \left[ \begin{matrix} 1 & 2 & -1 & -6 \\ 0 & -3 & -1 & 3\\ 0 & 1 & 1 & 1 \end{matrix} \right]

[121601110313][121601110026][121601110013] \Longrightarrow \left[ \begin{matrix} 1 & 2 & -1 & -6 \\ 0 & 1 & 1 & 1\\ 0 & -3 & -1 & 3 \end{matrix} \right] \Longrightarrow \left[ \begin{matrix} 1 & 2 & -1 & -6 \\ 0 & 1 & 1 & 1\\ 0 & 0 & 2 & 6 \end{matrix} \right] \Longrightarrow \left[ \begin{matrix} 1 & 2 & -1 & -6 \\ 0 & 1 & 1 & 1\\ 0 & 0 & 1 & 3 \end{matrix} \right]
最后我们得到一个“阶梯形矩阵”,它的系数矩阵部分被称为"上三角矩阵",名字来源其形状,这个矩阵表达的信息是:
[121601110013]{x1+2x2x3=6          x2+x3=1                   x3=3 \left[ \begin{array}{ccc|c} 1 & 2 & -1 & -6 \\ 0 & 1 & 1 & 1\\ 0 & 0 & 1 & 3 \end{array} \right] \Longrightarrow \begin{cases} x_1 + 2x_2-x_3=-6\\ \ \ \ \ \ \ \ \ \ \ x_2+x_3=1\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ x_3=3\end{cases}
因此,我们已经知道了最后一个未知量的值,从下往上一次带回方程,即可得到每个未知量的解。事实上,该矩阵也可以进一步简化:
[121601110013][120301110013][100101020013] \left[ \begin{matrix} 1 & 2 & -1 & -6 \\ 0 & 1 & 1 & 1\\ 0 & 0 & 1 & 3 \end{matrix} \right] \Longrightarrow \left[ \begin{matrix} 1 & 2 & 0 & -3 \\ 0 & 1 & 1 & 1\\ 0 & 0 & 1 & 3 \end{matrix} \right] \Longrightarrow \left[ \begin{array}{ccc|c} 1 & 0 & 0 & 1 \\ 0 & 1 & 0 & -2\\ 0 & 0 & 1 & 3 \end{array} \right]
最后得到的矩阵叫“简化阶梯形矩阵”,它的系数矩阵部分是一个“对角矩阵”,名字来源于其形状。该矩阵已经直接给出了方程组的解。

通过初等行变换把增广矩阵变为简化梯形矩阵的线性方程组求解算法就是高斯消元算法。

高斯消元的算法的思想就是,对于每个未知量xix_i,找到一个xix_i的系数非零,但x1xi1x_1-x_{i-1}的系数都是零的方程,然后用初等行变换把其他方程的xix_i的系数全部消成零。
上面给出的例子是一种比较理想的情况。事实上,在高斯消元的过程中,可能遇到各种各样的情况。

首先,在高斯消元过程中,可能出现0=d0 = d这样的方程,其中dd是一个非零常数。这表明某些方程之间存在矛盾,方程组无解。

其次,有可能找不到一个xix_i的系数非零,但x1xi1x_1-x_{i-1}的系数都是零的方程。这是我们要重点讨论的情况,例如:
{x1+2x2x3=32x1+4x28x3=0x12x2+6x3=2[121300660055][121300110000][120400110000] \begin{cases} x_1 + 2x_2-x_3=3\\ 2x_1+4x_2-8x_3=0\\-x_1-2x_2+6x_3=2\end{cases}\Longrightarrow \left[ \begin{matrix} 1 & 2 & -1 & 3 \\ 0 & 0 & -6 & -6\\ 0 & 0 & 5 & 5 \end{matrix} \right] \Longrightarrow \left[ \begin{matrix} 1 & 2 & -1 & 3 \\ 0 & 0 & 1 & 1\\ 0 & 0 & 0 & 0 \end{matrix} \right] \Longrightarrow \left[ \begin{array}{ccc|c} 1 & 2 & 0 & 4 \\ 0 & 0 & 1 & 1\\ 0 & 0 & 0 & 0 \end{array} \right]

在上例中,找不到x2x_2的系数非零,但x1x_1的系数为零的方程。方程组的解可以写作:
{x1=42x2x3=1\begin{cases} x_1=4-2x_2\\x_3=1\end{cases}
此时,x2x_2可以取任何值,都可以计算出一个对应的x1x_1,并且满足原方程组。换言之,原方程组有无穷个解。我们把x1,x3x_1,x_3这样的未知量称为主元,把x2x_2这样的未知量称为自由元

仔细分析可以发现,对于每个主元,整个简化阶梯星矩阵中有且仅有一个位置(i,j)(i,j)满足该主元的系数为零。第jj列的其他位置都是零,第ii行的第11j1j-1列都是零。

综上所述,在高斯消元后,若存在系数全为零,且常数不为零的行,则方程组无解。若系数不全为零的行恰好有nn个,则说明主元有nn个,方程组有唯一解。若系数不全为零的行有k<nk<n个,则说明主元有kk个,自由元有nkn-k个,方程组有无穷多个解。
【例题 1】Luogu P3389 【模板】高斯消元法
题意

给定一个线性方程组,对其求解

思路

采用Gauss-Jordan消元法,没有普通消元的回带过程,更简单,效率低一点。

原理:模拟线性代数中的“初等行列变换”。

保留对角线上的数,下面举个例子
[a10060a20100a26] \left[ \begin{array}{ccc|c} a_1 & 0 & 0 & -6 \\ 0 & a_2 & 0 & 1\\ 0 & 0 & a_2 & 6 \end{array} \right]
这样最后可以得到这样一个式子:
{a1x1=6a2x2=1a3x3=6{x1=6a1x2=1a2a3x3=6a3\begin{cases} a_1x_1=-6\\a_2x_2=1\\a_3x_3=6\end{cases} \Longrightarrow \begin{cases} x_1=\frac{-6}{a_1}\\x_2=\frac{1}{a_2}\\a_3x_3=\frac{6}{a_3}\end{cases}
算法步骤:

  1. 找到第 i (1<=i<=n)i \ (1<=i<=n)列最大值,把最大值所在的那一行和第 ii 行交换。
  2. 如果第 ii 列的最大值是0,那证明该方程组存在多组解,直接返回。
  3. 将第 ii 列 除了第 ii 行上的值 以外的所有其他行的值 通过“初等行变换”变为0。
  4. 重复以上步骤,直到退出或者i=ni = n遍历完所有列为止。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ll long long
using namespace std;

const int N = 110;
double a[N][N];
int n;

void print() {
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= n + 1; j++)
			printf("%.2f ",a[i][j]);
		puts("");
	}
	puts("");
}
void Gauss() {
	//遍历每一列 
	for(int j = 1; j <= n; j++) {
		int maxx = j;
		//找到当前列的最大值 
		for(int i = j + 1; i <= n; i++) {
			if(fabs(a[i][j]) > fabs(a[maxx][j]))
				maxx = i;
		}
		//如果当前列的最大是 0 则无解 
		if(a[maxx][j] == 0)	{
			printf("No Solution\n");
			return ;
		}
		//交换 j 行 和最大值所在行 
		for(int i = 1; i <= n + 1; i++)
			swap(a[j][i],a[maxx][i]);
		//高斯约旦消元的主要过程 
		for(int i = 1; i <= n; i++) {
			if(i != j) {
				double tmp = a[i][j] / a[j][j];
				for(int k = j + 1; k <= n + 1; k++){
					a[i][k]-=a[j][k]*tmp;
				}
				//print();
			}
		}
	}
	for(int i = 1; i <= n; i++){
		printf("%.2lf\n",a[i][n+1] / a[i][i]);
	}
}
int main() {
	scanf("%d",&n);
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= n + 1; j++) {
			scanf("%lf",&a[i][j]);
		}
	}
	Gauss();
	return 0;
}

/*
3
2 3 2 14
1 2 1 8
1 1 1 6
*/

【例题 2】luogu P4035 [JSOI2008]球形空间产生器
题意
  有一个球形空间产生器能够在n维空间中产生一个坚硬的球体。现在,你被困在了这个n维球体中,你只知道球
面上n+1个点的座标,你需要以最快的速度确定这个n维球体的球心座标,以便于摧毁这个球形空间产生器。
思路
我们设球心的座标为(x1,x2,x3,...,xn)(x_1,x_2,x_3,...,x_n),第ii个点的座标为(ai1,ai2,ai3,...,ain)(a_{i1},a_{i2},a_{i3},...,a_{in}),球的半径为RR

那么有
j=1n(aijxj)2=R\displaystyle\sum_{j =1}^n(a_{ij}-x_j)^2=R
展开化简后得

{a112a212+2(a21a11)x1+a122a222+2(a22a12)x2+...+a1n2a2n2+2(a2na1n)xn=0a212a312+2(a31a21)x1+a222a322+2(a32a22)x2+...+a2n2a3n2+2(a3na2n)xn=0...an 12an+1 12+2(an+1 1an1)x1+an22an+1 22+2(an+1 2an2)x2+...+ann2an+1 n2+2(an+1 nann)xn=0\begin{cases} a_{11}^2-a_{21}^2+2(a_{21}-a_{11})x_1+a_{12}^2-a_{22}^2+2(a_{22}-a_{12})x_2+...+a_{1n}^2-a_{2n}^2+2(a_{2n}-a_{1n})x_n=0 \\ a_{21}^2-a_{31}^2+2(a_{31}-a_{21})x_1+a_{22}^2-a_{32}^2+2(a_{32}-a_{22})x_2+...+a_{2n}^2-a_{3n}^2+2(a_{3n}-a_{2n})x_n=0 \\... \\a_{n\ 1}^2-a_{n+1\ 1}^2+2(a_{n+1\ 1}-a_{n1})x_1+a_{n2}^2-a_{n+1\ 2}^2+2(a_{n+1\ 2}-a_{n2})x_2+...+a_{nn}^2-a_{n+1\ n}^2+2(a_{n+1\ n}-a_{nn})x_n=0\end{cases}

移项得

{2(a21a11)x1+2(a22a12)x2+...+2(a2na1n)xn=a212a112+a222a122+...+a2n2a1n22(a31a21)x1+2(a32a22)x2+...+2(a3na2n)xn=a312a212+a322a222+...+a3n2a2n2...2(an+1 1an1)x1+2(an+1 2an2)x2+...+2(an+1 nann)xn=an+1 12an12+an+1 22an22+...+an+1 n2ann2\begin{cases}2(a_{21}-a_{11})x_1+2(a_{22}-a_{12})x_2+...+2(a_{2n}-a_{1n})x_n= a_{21}^2-a_{11}^2+a_{22}^2-a_{12}^2+...+a_{2n}^2-a_{1n}^2 \\ 2(a_{31}-a_{21})x_1+2(a_{32}-a_{22})x_2+...+2(a_{3n}-a_{2n})x_n= a_{31}^2-a_{21}^2+a_{32}^2-a_{22}^2+...+a_{3n}^2-a_{2n}^2 \\... \\ 2(a_{n+1\ 1}-a_{n1})x_1+2(a_{n+1\ 2}-a_{n2})x_2+...+2(a_{n+1\ n}-a_{nn})x_n= a_{n+1\ 1}^2-a_{n1}^2+a_{n+1\ 2}^2-a_{n2}^2+...+a_{n+1\ n}^2-a_{nn}^2\end{cases}

这样就很明显的是一个线性方程组,那么我们采用高斯消元

#include <cstdio>
#include <string>
#include <algorithm>
#include <cmath>
#define ll long long
using namespace std;

const int N = 120;
int n;
double a[N][N] = {0},b[N][N] = {0};

void Gauss(){
	for(int j = 1; j <= n; j++){
		int maxx = j;
		//找到该列的最大行 
		for(int i = j + 1; i <= n; i++){
			if(fabs(b[i][j]) > fabs(b[maxx][j])) maxx = i;
		}
		//如果最大值为 0 无解
//		if(b[maxx][j] == 0){
//			printf("no smoking!\n");
//			return ;		
//		} 
		//交换 
		for(int i = 1; i <= n + 1; i++)
			swap(b[j][i],b[maxx][i]);
		//高斯约旦消元 
		for(int i = 1; i <= n; i++){
			if(i != j){
				double tmp = b[i][j] / b[j][j];
				for(int k = j + 1; k <= n + 1; k++)
					b[i][k] -= b[j][k] * tmp;
			}
		}
	}
}

void solve(){
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= n; j++){
			b[i][j] = 2 * (a[i+1][j] - a[i][j]); 
			b[i][n + 1] += (a[i+1][j] - a[i][j]) * (a[i+1][j] + a[i][j]);
		}
	}
//	for(int i = 1; i <= n; i++){
//		for(int j = 1; j <= n + 1; j++)
//			printf("%.1lf ",b[i][j]);
//		puts("");
//	}
	Gauss();
	for(int i = 1; i <= n; i++){
		printf("%.3lf ",b[i][n + 1] / b[i][i]);
	}
}
int main(){
	scanf("%d",&n);
	for(int i = 1 ; i <= n + 1; ++i){
		for(int j  = 1; j <= n; ++j){
			scanf("%lf",&a[i][j]);
		}
	}
	solve();
	return 0;
} 

【例题 3】POJ 1830 开关问题
题意

有N个相同的开关,每个开关都与某些开关有着联系,每当你打开或者关闭某个开关的时候,其他的与此开关相关联的开关也会相应地发生变化,即这些相联系的开关的状态如果原来为开就变为关,如果为关就变为开。你的目标是经过若干次开关操作后使得最后N个开关达到一个特定的状态。对于任意一个开关,最多只能进行一次开关操作。你的任务是,计算有多少种可以达到指定状态的方法。(不计开关操作的顺序)

思路
xix_i 表示第 ii 个开关的操作情况,xi=1x_i=1 表示按了这个开关,xi=0x_i=0 表示没有按。再统计 ai,ja_{i,j} 表示第 ii个开关和第 jj 个开关的联系情况,ai,j=1a_{i,j}=1表示按下jj会影响 ii 的状态,ai,j=0a_{i,j}=0表示不会影响,特别地,令ai,i=1a_{i,i}=1
一个开关最后的状态 dstidst_i ,取决于它最初的状态 srcisrc_i,以及所有与它有练习的开关的操作情况只想异或运算的结果。可列出异或方程组:
{a1,1x1a1,2x2...a1,nxn=src1dst1a2,1x1a2,2x2...a1,nxn=src2dst2...an,1x1an,2x2...an,nxn=srcndstn\begin{cases} a_{1,1}x_1\bigoplus a_{1,2}x_2\bigoplus...\bigoplus a_{1,n}x_n=src_1\bigoplus dst_1 \\a_{2,1}x_1\bigoplus a_{2,2}x_2\bigoplus...\bigoplus a_{1,n}x_n=src_2\bigoplus dst_2 \\... \\a_{n,1}x_1\bigoplus a_{n,2}x_2\bigoplus...\bigoplus a_{n,n}x_n=src_n\bigoplus dst_n\end{cases}
异或其实就是不进位加法,我们仍然可以写出增广矩阵,矩阵中的每个值要么是 00,要么是 11。然后,在只想高斯消元的过程中,把加、减法替换成异或,且不需要执行乘法。最终我们可以得到该异或方程组对应的简化阶梯形矩阵。若存在形如0=10 = 1的方程,则方程组无解。否则,因为自由元可以取0011,所以方程组解的数量就是2cnt2^{cnt},其中 cntcnt 为自由元的个数。
【朴素算法】

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>
#define ll long long 
using namespace std;

const int N = 33;
int cas,n,ans,x,y,cnt;
int a[N][N];
int s[N],e[N],freex[N];

void debug() {
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= n; j++)
			printf("%d ",a[i][j]);
		puts("");
	}
}
int Gauss() {
	
	int col = 0;//当前列
	int num = 0;
	int k;//当前行
	for(k = 1; k <= n && col <= n; k++,col++){
		int maxx_row = k;
		for(int i = k + 1; i <= n; i++){
			if(abs(a[i][col] > abs(a[maxx_row][col]))) maxx_row = i;
		}
		//交换最大 
		if(maxx_row != k)
			for(int j = 1; j <= n + 1; j++)
				swap(a[k][j],a[maxx_row][j]); 
		if(a[k][col] == 0){
			freex[++num] = col;
			k--;
			continue;
		} 
		//高斯消元主要过程
		for(int i = k + 1; i <= n; i++){
			if(a[i][col] != 0){
				for(int j = col; j <= n + 1; j++)
					a[i][j] ^= a[k][j];
			}
		} 
	}
	
	//无解
	//printf("last_k = %d n = %d col = %d \n",k,n,col);
	for(int i = k; i <= n; i++)
		if(a[i][col] != 0) return -1;
	return n - k + 1;  
}
int main() {
	scanf("%d",&cas);
	while(cas--) {
		scanf("%d",&n);
		memset(a,0,sizeof(a));
		for(int i = 1; i <= n; i++) scanf("%d",&s[i]);
		for(int i = 1; i <= n; i++) scanf("%d",&e[i]);
		for(int i = 1; i <= n; i++) a[i][n + 1] = s[i] ^ e[i],a[i][i] = 1;
		while(scanf("%d%d",&x,&y) && x && y) {
			a[y][x] = 1;
		}
		//debug();
		ans = Gauss();
		if(ans == -1) puts("Oh,it's impossible~!!");
		else printf("%d\n",1<<ans);
	}
	return 0;
}


在编写程序的时候,为了简便、高效,可以把增广矩阵的每一行进行状态压缩,用一个int类型的整数表示n+1n + 1 位二进制数,其中第 00 位为增广矩阵的最后一列的常数,第11$n$位分别为增广矩阵第$i$nn列的系数。

算法进阶指南代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ll long long
using namespace std;

int a[100],n,cas,ans;
int main() {
	scanf("%d",&cas);
	while(cas--) {
		scanf("%d",&n);
		for(int i  = 1; i <= n; i++) scanf("%d",&a[i]);
		for(int i = 1,j; i <= n; i++) {
			scanf("%d",&j);
			a[i] ^= j;
			a[i] |= 1 << i; //a[i][i] = 1;
		}
		int x,y;
		while(~scanf("%d%d",&x,&y) && x && y) {
			a[y] |= 1 << x;//a[y][x] = 1;
		}
		ans = 1;
		for(int i = 1; i <= n; i++) {
			//找到最大的一个a[i]
			for(int j = i + 1; j <= n; j++)
				if(a[j] > a[i]) swap(a[i],a[j]);
			//消元完毕,有i-1个主元,n-i+1个自由元
			if(a[i] == 0) {
				ans = 1 << (n - i + 1);
				break;
			}
			//出现0=1的方程,无解
			if(a[i] == 1) {
				ans = 0;
				break;
			}
			//a[i]最高位的1作为主元,消去其他方程该位的系数
			for(int k = n; k; k--) {
				if(a[i] >> k & 1) {
					for(int j = 1; j <= n; j++) {
						if(i != j && (a[j] >> k & 1)) a[j] ^= a[i];
					}
					break;
				}

			}
		}
		if(ans == 0) puts("Oh,it's impossible~!!");
		else printf("%d\n",ans);
	}
	return 0;
}

本文参考:
《算法竞赛进阶指南》 李煜东

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章