高斯消元是一種求解線性方程組的方法。所謂線性方程組,是由個元一次方程共同構成的。線性方程組的所有西數可以寫成一個行列的“係數矩陣”,再加上每個方程等號右側的常數,可以寫成一個 行 列的“增廣矩陣”,例如:
求解方程組的步驟可以概括成對“增廣矩陣”的三類操作:
1.用一個非零的數乘某一行;
2.把其中一行的若干倍加到另一行上;
3.交換兩行的位置。
這其實就是我們線性代數中矩陣的“初等行變換”。同理,我們也可以定義矩陣的“初等列變換”。用若干次初等行變換求解上面的方程組,過程如下:
最後我們得到一個“階梯形矩陣”,它的係數矩陣部分被稱爲"上三角矩陣",名字來源其形狀,這個矩陣表達的信息是:
因此,我們已經知道了最後一個未知量的值,從下往上一次帶回方程,即可得到每個未知量的解。事實上,該矩陣也可以進一步簡化:
最後得到的矩陣叫“簡化階梯形矩陣”,它的係數矩陣部分是一個“對角矩陣”,名字來源於其形狀。該矩陣已經直接給出了方程組的解。
通過初等行變換把增廣矩陣變爲簡化梯形矩陣的線性方程組求解算法就是高斯消元算法。
高斯消元的算法的思想就是,對於每個未知量,找到一個的係數非零,但的係數都是零的方程,然後用初等行變換把其他方程的的係數全部消成零。
上面給出的例子是一種比較理想的情況。事實上,在高斯消元的過程中,可能遇到各種各樣的情況。
首先,在高斯消元過程中,可能出現這樣的方程,其中是一個非零常數。這表明某些方程之間存在矛盾,方程組無解。
其次,有可能找不到一個的係數非零,但的係數都是零的方程。這是我們要重點討論的情況,例如:
在上例中,找不到的係數非零,但的係數爲零的方程。方程組的解可以寫作:
此時,可以取任何值,都可以計算出一個對應的,並且滿足原方程組。換言之,原方程組有無窮個解。我們把這樣的未知量稱爲主元,把這樣的未知量稱爲自由元。
仔細分析可以發現,對於每個主元,整個簡化階梯星矩陣中有且僅有一個位置滿足該主元的係數爲零。第列的其他位置都是零,第行的第到列都是零。
綜上所述,在高斯消元后,若存在係數全爲零,且常數不爲零的行,則方程組無解。若係數不全爲零的行恰好有個,則說明主元有個,方程組有唯一解。若係數不全爲零的行有個,則說明主元有個,自由元有個,方程組有無窮多個解。
【例題 1】Luogu P3389 【模板】高斯消元法
題意
給定一個線性方程組,對其求解
思路
採用Gauss-Jordan消元法,沒有普通消元的迴帶過程,更簡單,效率低一點。
原理:模擬線性代數中的“初等行列變換”。
保留對角線上的數,下面舉個例子
這樣最後可以得到這樣一個式子:
算法步驟:
- 找到第 列最大值,把最大值所在的那一行和第 行交換。
- 如果第 列的最大值是0,那證明該方程組存在多組解,直接返回。
- 將第 列 除了第 行上的值 以外的所有其他行的值 通過“初等行變換”變爲0。
- 重複以上步驟,直到退出或者遍歷完所有列爲止。
#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維球體的球心座標,以便於摧毀這個球形空間產生器。
思路
我們設球心的座標爲,第個點的座標爲,球的半徑爲。
那麼有
展開化簡後得
移項得
這樣就很明顯的是一個線性方程組,那麼我們採用高斯消元
#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個開關達到一個特定的狀態。對於任意一個開關,最多隻能進行一次開關操作。你的任務是,計算有多少種可以達到指定狀態的方法。(不計開關操作的順序)
思路
設 表示第 個開關的操作情況, 表示按了這個開關, 表示沒有按。再統計 表示第 個開關和第 個開關的聯繫情況,表示按下會影響 的狀態,表示不會影響,特別地,令。
一個開關最後的狀態 ,取決於它最初的狀態 ,以及所有與它有練習的開關的操作情況只想異或運算的結果。可列出異或方程組:
異或其實就是不進位加法,我們仍然可以寫出增廣矩陣,矩陣中的每個值要麼是 ,要麼是 。然後,在只想高斯消元的過程中,把加、減法替換成異或,且不需要執行乘法。最終我們可以得到該異或方程組對應的簡化階梯形矩陣。若存在形如的方程,則方程組無解。否則,因爲自由元可以取或,所以方程組解的數量就是,其中 爲自由元的個數。
【樸素算法】
#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$位分別爲增廣矩陣第$i$列的係數。
算法進階指南代碼:
#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;
}
本文參考:
《算法競賽進階指南》 李煜東