快樂地打牢基礎(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;
}

本文參考:
《算法競賽進階指南》 李煜東

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