消元法
線性系統,在高中稱爲 多元一次方程組,因爲在線性代數裏,我們把矩陣看成系統,而這些方程組的未知數都只有一次,所以就了線性系統。
我們把真實世界的問題,轉換爲線性方程(線性系統),研究線性系統,也就是解這些方程,解出來問題就解決了。
高斯消元法
初中的時候,我們學習了高斯消元法。
高斯消元法主要分爲兩步,
- 消元:要減少某些方程中元的數量;
- 回代:是把已知的解代入到方程式中,求出其他未知的解。
如果方程和元的數量很小,那麼高斯消元法並不難理解。
可是如果方程和元的數量很多,整個過程就變得比較繁瑣了。
而後,自然而然人們想到了對計算進行批處理,可以把高斯消元法轉爲矩陣的操作。
比如:
用矩陣表示(矩陣乘法):
我們再把係數矩陣和等式右邊的矩陣放在一起:
這個矩陣叫【增廣矩陣】,就是在係數矩陣右邊增加了一列等號右邊的解。
通過矩陣解線性方程,本質也是消元法,要做的就是把消元的過程封裝到矩陣裏。
用消元法解方程,因爲操作的是矩陣,所以消元法的書寫方式有點變化:
- 原【一個方程的左右倆邊同時乘以一個常數】 變爲【矩陣的某一行乘以一個常數】
- 原【一個方程加(或減)另一個方程】 變爲【矩陣的一行加(或減)另一行】
- 原【交換倆個方程的位置】 變爲【交換矩陣的倆行】
也就是說,計算方式是完全一致的,只不過操作對象從方程變成了矩陣,因爲矩陣是可以批處理運算的工具。
我快速手算一下:
-
從上到下,以矩陣的第一行爲基準,消去第二、第三行的第一個元素(係數化爲 )
第一步 ,把第二行的第一個元素化成 ;第二步 ,把第三行的第一個元素化成 ;
第三步 ,把第三行的第二個元素化成 ;
第三步後矩陣的第三行是 ,而後給第三行左右倆邊同時除以 ,最後的結果就是上圖最後一個矩陣。
現在我們知道,最後一個未知數,這是消元法的消元部分,接着是回代部分,由下往上回代解出其餘未知數:)。
總結一下,高斯消元法。
消元部分:
-
從上到下,以矩陣的第一行爲基準,消去第二、第三行的第一個元素(係數化爲 ),這就代表消去了一個元;
因爲他們都是基於矩陣第一行第一個位置消元的,所以這個位置也被稱爲【主元】。
不僅第一行第一個位置是主元,第二行第二個、第三行第三個、第 行第 個都是主元。P.S. 只要在這個位置,任何不爲 0,就可以當主元。如果第一行第一個元素爲 ,就需要【交換矩陣的倆行】。若第一行主元不爲 的元素,先化爲 ,再運算。
首先就是把這些主元位置的係數化爲 ,之後其他行就可以使用 【矩陣的某一行乘以一個常數】、【矩陣的一行加(或減)另一行】把每行主元左邊的元素化爲 。最後,矩陣會變成一個階梯型的矩陣,比如這樣。
回代部分:
- 由下往上回代解出其餘未知數,把最後一個未知數代入倒數第二行,得到倒數第二個未知數的解;再把倒數第一、倒數第二的未知數代入到第一個裏面,得到 個未知數的解。
高斯消元法實現:
// 運行:命令行輸入 gcc/g++ 當前源文件.c/cpp
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
double **A = NULL, *b = NULL, *x = NULL;
unsigned int RANK = 4;
unsigned int makematrix(){
unsigned int r, c;
printf("請輸入矩陣行列數,用空格隔開:");
scanf("%d %d", &r, &c);
A = (double**)malloc(sizeof(double*)*r);
// 創建一個指針數組,把指針數組的地址賦值給a ,*r是乘以r的意思
for (int i = 0; i < r; i++)
A[i] = (double*)malloc(sizeof(double)*c);
// 給第二維分配空間
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++)
A[i][j] = 0.0;
}
b = (double*)malloc(sizeof(double)*r);
for (int i = 0; i < r; i++){
b[i] = 0.0;
}
x = (double*)malloc(sizeof(double)*c);
for (int i = 0; i < c; i++){
x[i] = 0.0;
}
return r;
// 一般都是輸入方陣,返回行數也闊以
}
void getmatrix(void) {
// 輸入矩陣並呈現
printf("\n\n請按行從左到右依次輸入係數矩陣A,不同元素用空格隔開:\n");
for (int i = 0; i < RANK; i++) {
for(int j = 0;j<RANK;j++) {
scanf("%lf", &A[i][j]);
}
}
printf("\n\n係數矩陣如下:\n");
for (int i = 0; i < RANK; i++) {
for (int j = 0; j<RANK; j++) {
printf("%g\t",A[i][j]);
}
putchar('\n');
}
printf("\n\n請按從上到下依次輸入常數列b,不同元素用空格隔開:\n");
for (int i= 0; i<RANK; i++) {
scanf("%lf", &b[i]);
}
printf("常數列如下\n");
for (int i = 0; i<RANK; i++) {
printf("%g\t", b[i]);
}
putchar('\n');
}
void Gauss_calculation(void) {
// Gauss消去法解線性方程組
double get_A = 0.0;
printf("\n\n利用以上A與b組成的增廣陣進行高斯消去法計算方程組\n");
for (int i = 1; i < RANK; i++) {
for (int j = i; j<RANK; j++) {
get_A = A[j][i - 1] / A[i - 1][i - 1];
b[j] = b[j] - get_A * b[i - 1];
for (int k = i-1; k < RANK; k++) {
A[j][k] = A[j][k] - get_A * A[i-1][k];
}
}
}
printf("\n\n順序消元后的上三角係數增廣矩陣如下:\n");
for (int i = 0; i < RANK; i++) {
for (int j = 0; j<RANK; j++) {
printf("%g\t", A[i][j]);
}
printf(" %g", b[i]);
putchar('\n');
}
printf("\n\n利用回代法求解上三角方程組,解得:\n\n");
for (int i = 0; i < RANK; i++) {
double get_x = 0.0;
for (int j = 0; j < RANK; j++) {
get_x = get_x + A[RANK-1-i][j]*x[j];
// 把左邊全部加起來了,下面需要多減去一次Xn*Ann
}
x[RANK - 1 - i] = (b[RANK - 1 - i] - get_x + A[RANK - 1 - i][RANK - 1 - i] * x[RANK - 1 - i]) / A[RANK - 1 - i][RANK - 1 - i];
}
for (int i = 0; i < RANK; i++) {
printf("x%d = %5g\n", i + 1, x[i]);
}
printf("\n\n計算完成,按回車退出程序或按1重新輸入矩陣\n");
}
int main() {
RANK = makematrix();
getmatrix();
Gauss_calculation();
// P.S. double **A, *b, *x 沒釋放
return 0;
}
高斯消元法演示:
高斯-約旦消元法
高斯消元法的問題,只能通過矩陣得到最後一個未知數的解,之後還得一直回代依次得到所有結果,那有木有什麼方法一次性把結果搗鼓出來呢?
有呀,高斯-約旦消元法。
回到高斯消元法的消元后,其實我們也可以不用回代,只有把 化爲 即可。
我們倒着進行消元即可,初始的消元是從第一行開始,從上往下以第一行的主元爲基準,把主元下面的位置都化爲 ,現在反過來從最後一行開始,從下往上以最後一行的主元爲基準,把主元上面的位置都化爲 。
高斯-約旦消元法:
- 高斯消元:前向過程,從上到下
- 約旦消元:後向過程,從下到上
高斯-約旦消元法實現:
// 運行:在命令行輸入 g++ -std=c++11 當前源文件.cpp
#include<cstdio>
#include<cmath>
#include<algorithm>
#define eps 1e-8
using namespace std;
double a[55][55], ans[55]; // a爲增廣矩陣
int d;
int gauss_jordan(int n) {
int r, w = 0;
for (int i = 0; i < n && w < n; w++, i++) {
// 進行到第i列,第w行
int r = w;
for (int j = w + 1; j < n; j++)
if (fabs(a[j][i]) > fabs(a[r][i]))
r = j; // 找到當前列絕對值最大的行
if (fabs(a[r][i]) < eps) {
w--;
continue;
} // 當前列值都爲0,跨過當前步
if (r != w)
for (int j = 0; j <= n; j++)
swap(a[r][j], a[w][j]);
// 交換當前列絕對值最大的行和沒計算過的第一行,使用最大值運算,可減少誤差
for (int k = 0; k < n; k++) {
// 消去當前列(除本行外)
if (k != w)
for (int j = n; j >= w; j--)
a[k][j] -= a[k][i] / a[w][i] * a[w][j];
}
}
return w;
}
int main() {
int n;
scanf("%d", &n); // 默認輸入的矩陣是方陣,行數等於列數,但這個限制也不是必須的,可以打破
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) { // 因爲解的那一列也一起輸入,所以加了一行
scanf("%lf", a[i] + j); // C/C++ 裏其實只有一維數組,a[i] + j = a[i][j]
}
}
d = gauss_jordan(n);
d--;
for (int i = 0; i < n; i++) {
// 有一個方程 = 右邊不爲0 = 左邊爲0,則無解
bool d = 1;
for (int j = i; j < n; j++)
d &= (fabs(a[i][j]) < eps);
if (d && fabs(a[i][n]) > eps) {
putchar('-1');
return 0;
}
}
for (int i = 0; i < n; i++) {
// 消元后有變量在多個方程中出現,則有多個解
int max1 = 0;
for (int j = i; j < n; j++)
if (fabs(a[i][j]) > eps)
max1++;
if (max1 > 1) {
putchar('0');
return 0;
}
}
for (int i = 0; i < n; i++)
ans[i] = a[i][n] / a[i][i];
putchar('\n');
for (int i = 0; i < n; i++) {
if (fabs(ans[i]) < eps)
printf("x%d = 0\n", i + 1);
else
printf("x%d = %5.2lf\n", i + 1, ans[i]);
}
return 0;
}
高斯-約旦消元法演示:
工程應用:化學方程式配平
問題描述:給出一個未配平的化學方程式,根據質量守恆定律對其分配,不考慮化合價問題
示例:)
- 輸入:Cu+HNO3=Cu(NO3)2+NO+H2O(中間不要加入空格)
- 輸出:3Cu+8HNO3=3Cu(NO3)2+2NO+4H2O
分析:
化學反應遵循質量守恆定律,反應前後的原子種類和數量保持不變,根據這,採用【待定係數法】來配平化學方程式。
具體的操作,給方程式中的每一項設一個待定係數,列出方程組。
比如說,輸入示例方程式,分別設 的係數爲 。
由元素 的守恆,得到方程組:
但方程組只有 個方程,未知數卻有 個,無法求出唯一確定的一組解。
因爲化學方程式係數間只是一種比例關係,可令 ,得:
各項係數都乘 ,得到最後的結果。
用計算機實現,應該用矩陣代替,消元法相同只是從操作方程變成了操作矩陣。
此外,還得將化學方程式轉爲線性方程組,難點在於去括號。
// 運行:在命令行輸入 gcc 當前源文件.c
#include <stdio.h>
#include <string.h>
#include <math.h>
const double eps = 1e-10;
int n, m, hash[ 1<<20 ];
double a[ 205 ] [ 205 ];
char z[ 205 ];
int anw, now;
void Swap(double a, double b) {
double t = a;
a = b;
b = t;
}
int get_number( int &i ) {
int s = 0;
while( '0' <= z[ i ] && '9' >= z[ i ] )
s = ( s << 3 ) + ( s << 1 ) + z[ i ++ ] - '0';
return s>1? s:1;
}
int get_str( int &i ) {
if( z[ i ] > 'Z' || z[ i ] < 'A' )
return -1;
int s = z[ i ++ ];
while( 'a' <= z[ i ] && z[ i ] <= 'z' )
s = s * 10007 + z[ i ++ ];
return s & ( ( 1<<20 )-1 );
}
void countt( int l, int r, int f) {
if( l == r ) return;
int i = l, j = r;
while( i < r - 1 && z[ i ] != '(' )
i ++;
while( j > l && z[ j ] != ')' )
j --;
if( z[ i ] == '(' ){
countt(l, i, f);
int w = j + 1, s = get_number(w);
countt(i+1, j, f*s);
countt(w, r, f);
return;
}
for( i = l; i < r; ){
int str = get_str(i);
if( ! hash[ str ] )
hash[ str ] = ++n;
int hs = hash[ str ];
a[ hs ] [ m ] += f * get_number(i);
}
}
void init( ){
printf("化學方程式配平前>> ");
scanf("%s",z);
int l = strlen(z), f = 1;
z[ l ] = '#';
for( int i = 0; i < l; ){
int j = i;
while( z[ j ] != '+' && z[ j ] != '=' && z[ j ] != '#' )
j ++;
m ++;
countt(i, j, f);
if( z[ j ] == '=' )
f *= -1;
i = j +1;
}
}
void gs( ){ // 高斯消元
for( int j = 1, i; j < m; j ++ ){
for( i = now + 1; fabs(a[ i ] [ j ]) < eps && i <= n; i ++ );
if( i > n ){
anw = -1;
continue;
}
now ++;
for( int k = 1; k <= m; k ++ )
Swap(a[i][k], a[now][k]);
double ss = a[now][j];
for( int k = 1; k <= m; k ++ )
a[now][k] /= ss;
for( i = 1; i <= n; i ++ )
if( fabs(a[i][j]) > eps && i != now ){
double s = a[i][j];
for( int k = 1; k <= m; k ++ )
a[i][k] -= a[now][k] * s;
}
}
}
int ans[205];
void solve( ){
for( int i = now+1; i <= n; i ++ )
if( fabs(a[i][m])>eps ){
printf("無解\n");
return;
}
if( anw == -1 ) puts("多組解法");
for( int i = 1; i <= 1000; i ++ ){
int p = 1;
for( int j = 1; j < m; j ++ ){
if( fabs(int(a[j][m] * i * -1 + 0.5) - a[j][m] * i * -1) > eps )
p = 0;
}
if( p == 0 ) continue;
for( int j = 1; j < m; j ++ )
ans[j] = int( a[j][m] * i * -1 + 0.5 );
ans[m] = i;
printf("化學方程式配平後>> ");
if( ans[1] != 1 ) printf("%d", ans[1]);
int l = strlen(z), k = 1;
for( int j = 0; j < l-1; j ++ ){
putchar(z[j]);
if( z[j] == '=' || z[j] == '+' )
printf("%d",ans[++ k]);
}
break;
}
}
int main(void){
init( );
gs( );
solve( );
return 0;
}
逆矩陣求解
求解線性方程組除了消元法之外,逆矩陣也可求解。
在《矩陣實驗:圖形圖像處理》,着重寫了把矩陣看成一種對向量的函數、變換。
上面的方程,從線性系統的角度來看線性方程組的求解過程:
線性系統相當於:
上面的方程,從線性變換的角度來看線性方程組的求解過程:
線性變換相當於;
- ,矩陣 是變換。
看線性系統,已知變換矩陣、輸出矩陣,求解輸入矩陣。
那求解線性方程組就是一個逆變換的過程;因此,有一個逆矩陣表徵這種變換。
逆矩陣:若 ,則稱 互爲逆矩陣;記作:。
如果我們在等式倆邊都乘,式子如下:
這個式子在幾何上,變換來變換去,最後回到初始狀態:
參照這種思想(矩陣是一種變換的思想), 可以用 來求!!
求線性方程組就變成了求解某個矩陣的逆矩陣。
現在,唯一要明白的是:逆矩陣怎麼求 ?
矩陣的逆
除零以外,一個數字乘以這個數字的倒數(逆)等於一:
這是在數字系統裏,事實上,矩陣也可以逆:
- , 是單位矩陣,相當於數字裏面的 。
如果矩陣滿足上述 ,則稱 是 的逆矩陣,記做:
大部分矩陣都是有逆的:
- 有逆的矩陣叫
可逆矩陣
or非奇異矩陣
- 不可逆的矩陣叫
不可逆矩陣
or奇異矩陣
因爲矩陣乘法不滿足交換律,所以可逆分成了倆種:
- 左逆:,只有左逆,是不可逆矩陣;
- 右逆:,只有右逆,是不可逆矩陣;
可逆矩陣是同時有左逆和右逆,只有左逆或只有右逆都不叫可逆。
- 只有方陣可逆(行數 = 列數),因爲對於一個方陣來說,左逆和右逆一定是同時存在的。
本科的線性代數主要研究方陣(除了線性系統)。
矩陣求逆:
# 運行:在命令行裏輸入 python 當前源文件.py
import numpy as np
from scipy import linalg
A = np.array([[1, 35, 0],
[0, 2, 3],
[0, 0, 4]])
A_n = linalg.inv(A)
print(A_n)
print(np.dot(A, A_n))
運行結果:
[[ 1. -17.5 13.125]
[ 0. 0.5 -0.375]
[ 0. 0. 0.25 ]]
[[ 1. 0. 0.]
[ 0. 1. 0.]
[ 0. 0. 1.]]
C++
也有專屬的線代庫 armadillo,因爲有數據類型有精度限制可以用 Boost.Multiprecision,用於科學計算也就很方便。
解的結構
消元法所做的是把一個增廣矩陣變換爲一個階梯型矩陣。
階梯型矩陣:
- 非零行第一個元素(主元)爲
- 主元所在列的其他元素均爲
滿足這種條件的矩陣,也被稱爲 “行最簡形式”。
線性方程組 通過 消元法 變成 行最簡形式,這樣就可以直接判斷方程組解的情況:
- 適定方程組:方程組有唯一解,理想情況,遇到的情況不多;
- 超定方程組:方程組無解,無論三個未知數取什麼解,方程始終不能滿足,因此方程組無解;遇到的概率很大;
- 欠定方程組:方程組有無數解,這種方程組的未知數有無數個解,因此實用價值不大。
一般的判定方法,是通過矩陣的秩來判斷方程組的類型。
- 欠定方程組(無數解):係數矩陣的秩 = 增廣矩陣的秩 < 未知數個數;
- 適定方程組(唯一解):係數矩陣的秩 = 增廣矩陣的秩 = 未知數個數;
- 超定方程組(無 解):係數矩陣的秩 增廣矩陣的秩 。
超定方程組近似估值
我們遇到的多是 超定方程組,因此學習一下 如何求超定方程的近似解。
先看一個比較簡單的問題。
設 ,能否找到一組 滿足:。
用矩陣表示:,也等同於 , (解出來是,適定方程組),解得(唯一解) :。
第二個問題,設 ,試問,能否找到一組 滿足:。
用矩陣表示:,也等同於 ,(解出來是,超定方程組),無解。
圖中的向量 ,脫離了 倆個向量的平面;這就從幾何上說滿足條件的未知數是無法找到的。
雖然無解,但卻可以找到一個最接近向量 的 向量 ,如下圖。
向量 - 向量 = 向量
幾何推導過程:
方程組表達式也可以推導:
- 方程組表達式:
- 在等式倆邊都乘一個
的轉置乘 必然是一個實對稱矩陣,實對稱矩陣必定可逆,等式的左右倆邊還可以乘 的轉置:。
工程應用:天體軌道參數估計
前置知識:《極座標、開普勒第一定律》。
根據開普勒第一定律,當忽略其他天體的重量吸引時,一個天體應該取橢圓、拋物線或雙曲線軌道。
在極座標 中,天體的位置滿足一個方程:
爲軌道常數, 是軌道偏心率;對於橢圓 ,對於拋物線 。
著名的哈雷彗星是一個橢圓,臺灣的鹿林彗星是一個拋物線。
天文學家利用太空望遠鏡🔭,觀測到一個新的天體,上圖的 原點 代表地球,座標是極座標系。
新天體,第一次觀測出現的位置( 得到極座標:):
連續觀測了五次,得到了五組數據(五個座標):
而後,要做的就是根據這些已有的數據推導天體的軌道方程!!
也就是,估計軌道常數參數、軌道偏心率。
因爲我們已經得到了五組 ,而天體的位置又必然滿足軌道方程,就形成了五組二元一次方程。
看這些方程的類型,就是超定方程組 — 係數還有小數,基本消不了。
用矩陣表示(矩陣乘法):
解得:。
所以,天體軌道方程爲:。
整理得:
觀測的數據越多,參數估計就越準確。
# 運行:在命令行輸入 python 當前源文件.py
import numpy as np
import matplotlib.pyplot as plt
# 1.準備好觀測到的 (θ,r) 數據 r = 貝塔 + e(r cosθ)
data = np.array([ [ 0.88, 3.00],
[ 1.10, 2.30],
[ 1.42, 1.65],
[ 1.77, 1.25],
[ 2.14, 1.01] ])
# 2.構建係數矩陣A
A = np.ones((data.shape[0],data.shape[1]))
for i in range(0,data.shape[0]): # i從 [0,5) 區間逐一取值
theta , r = data[i][0] , data[i][1]
A[i][1] = r * np.cos(theta)
print("Mat A:\n", A)
# 3.構造常數項矩陣b
b = data[:,1]
print("Mat b:\n", b)
# 4. 根據估值公式求解參數
x = np.dot(np.dot(np.linalg.inv(np.dot(A.T,A)), A.T),b)
print("--------------\nMat x:\n",x)
# 5. 畫出天體運行軌道
theta_ = np.linspace(0, 2*np.pi, 100)
r_ = x[0] / (1-x[1]* np.cos(theta_))
graph = plt.subplot(111, polar=True) # 1行1列圖像中的第一個
data_= data.T
graph.plot(data_[0,:],data_[1,:],'go') # 將已測量數據打上綠色的o('go')
graph.plot(theta_, r_ ,'r', linewidth=1)
plt.show()
效果演示: