1. 程式人生 > 實用技巧 >線性代數

線性代數

理論部分

  • 奇/偶排列

逆序對為奇數/偶數的排列

  • 矩陣單位元E

(i, i)為1,其餘為0.單位元其他矩陣,都為其他矩陣本身。

  • 線性無關(線性獨立):

可以這麼理解:

把一個n維向量理解成n元方程,比如(2, 1, 3)可以理解成2x + 1y + 3z。

兩個向量線性無關就是把一個方程通過加法或乘法無法得到另一個方程。

n個向量線性無關就是把n個方程中的任意n-1個進行加法或乘法,無法得到剩下的那個向量。

n元一次方程組(共n組)有唯一解,只有當那n個方程線性無關(暫且這麼說)

  • 秩:

又分為行秩和列秩。

秩 = 最大線性無關組數量。

一個矩陣A的列秩是A的線性獨立的縱列的極大數,通常表示為r(A)
- by ryc
  • 行列式的性質:
  1. 轉置後行列式不變

  2. 交換兩行後行列式為相反數

  3. 一行乘某個係數再加到另一行,行列式不變。

  4. 行列式中如果有兩行元素成比例,則此行列式等於零

  5. 行列式的某一行中所有的元素都乘同一數k,行列式乘k.

  • 行列式的(不常見)應用
  1. 無向圖生成樹個數(度數矩陣-鄰接矩陣 後刪最後一行一列的行列式)

  2. n個n維向量構成的平行2n面體的“超體積”(類似叉積)


9.10 Update

強烈推薦文章:線性代數入門;以及視訊:線性代數的本質

向量 & 線性組合 & 張成的空間 & 基

向量可以理解為箭頭或列矩陣(如 \(\begin{bmatrix} 2 \\ -5\\ 8 \end{bmatrix}\)

)。

線性組合:向量加減

張成的空間:兩向量通過組合能得到的所有向量的集合

集:該空間的線性無關向量集。

線性變換(矩陣)

實際上矩陣和線性變換是一個東西。

線性變換類似函式,輸入一個向量,輸出一個向量。
也可以理解為將一個空間(座標系)對映到另一個空間中,這是從整個空間的角度來理解的。例如旋轉,翻折等。但是必須是線性的(同時具有 \(f(x+y)=f(x)+f(y),f(ax)=af(x)\) 的性質)。

為了簡化問題,我們通常用基向量的變換來表示線性變換。而基向量的變換可以用若干列向量拼合成的矩陣來表示:\(\begin{bmatrix} x_1 & x_2\\ y_1 & y_2 \end{bmatrix}\)

。根據矩陣乘法定義,我們用一個向量乘這個變換矩陣得到的是新座標系中這個向量用原座標的表示(或者可以理解為原平面中的這個向量經過某種變換得到的新的向量):

\[\begin{bmatrix} a& c\\ b & d \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix}=\begin{bmatrix} x' \\ y' \end{bmatrix}\]

注意是從右向左乘。

一個典型的例子是曼哈頓距離轉切比雪夫距離。我們希望將整個平面順時針旋轉45°並且擴大 \(\sqrt 2\) 倍。基向量 \((0,1),(1,0)\) 將變化到: \((1,1),(1,-1)\),於是轉移矩陣為 \(\begin{bmatrix} 1 & 1\\ 1 & -1 \end{bmatrix}\),一個向量 \(\begin{bmatrix}x\\y\end{bmatrix}\) 的轉化過程可以表示為:

\[\begin{bmatrix} 1 & 1\\ 1 & -1 \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix}= \begin{bmatrix} x+y \\ x-y \end{bmatrix}\]

線性變換是可以複合的,即存在一種變換,等價於先做 A 變換,再做 B 變換。變換矩陣也可以複合:

\[... \begin{bmatrix} i & k\\ j & l \end{bmatrix} \begin{bmatrix} e & g\\ f & h \end{bmatrix} \begin{bmatrix} a & c\\ b & d \end{bmatrix} \]

行列式

行列式可以表示一個線性變換對空間的體積的拉伸倍數。如 \(det\bigg(\begin{bmatrix}1\ 3\\0\ 2\end{bmatrix}\bigg)=2\) 表示 \(\begin{bmatrix}1\ 3\\0\ 2\end{bmatrix}\) 這種線性變換會把原平面內面積為 \(S\) 的圖形的面積變成 \(2S\)

如果一個線性變換的行列式為 \(0\),那麼該線性變換之後空間會降維,例如可能會把一個平面上的所有點(向量)拍到一根直線上;如果一個線性變換的行列式為負數,說明這種線性變換會造成翻轉現象。

顯然,線性變換的複合的行列式為各線性變換行列式的乘積:\(det(M_1M_2)=det(M_1)det(M_2)\)

線性方程組與矩陣

線性方程組可以用矩陣的形式來表示。嚴格地說,應該可以表示成 \(Ax=v\) 的形式。(\(x,v\) 是向量,\(A\) 是係數矩陣)

秩 & 列空間 & 零空間 & 非方陣

秩表示線性變換後空間的維度。

列空間表示一個向量通過數乘能到達的所有向量的集合。

零空間為線上性變換後對映到原點的向量集合。

非方陣形如 \(\begin{bmatrix}a \ d \\b \ e\\c\ f\end{bmatrix}\) 或者 \(\begin{bmatrix}a\ c\ e\\b\ d\ f\end{bmatrix}\),表示一種將某維度的空間對映到另一維度的空間。注意即使從二維對映到了三維,也只能是三維中的一個平面。

點積及其對偶性

(二維)點積可以視為將一個二維向量對映到一根直線上的線性變換,因此可以寫成 \(\begin{bmatrix}a \ b\end{bmatrix}\) 的形式;而我們發現這跟直線恰好是 \(\begin{bmatrix} a\\b \end{bmatrix}\),於是有一種解釋為點積=投影 × 模長,而另一種解釋為 \(\begin{bmatrix} a \\ b \end{bmatrix} \cdot \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} ax \\ by \end{bmatrix}\)

對偶性我也說不清,建議看部落格或視訊。

叉積及其對偶性


以下是 OI 中常用的線性代數演算法。

高斯消元

每次用一行中的一個元,消去同一列中的所有元,最終消成對角矩陣。

但是可能會有無數解或無解的情況。比如,找不到第 \(j\) 列非 0,且其前面都被消去的一行,或者出現 \(0x_1 + 0x_2 + ... + 0x_n = 2.71828\) 的情況。

我們發現:當出現 \(0 = d\) 的情況時,說明方程組無解;否則,如果消元過程中出現 找不到第 \(j\) 列非 0,且其前面都被消去的一行 的情況時,說明方程組有無陣列解;否則,說明方程組有唯一解。(前提:n個元,n組方程)

同時,碰到 “找不到第 \(j\) 列非 0,且其前面都被消去的一行” 的情況 的次數還與矩陣的秩有關。矩陣的秩 = 最終有多少行的元的係數不全為0.

高斯消元有時也可以解決一些二進位制問題,如:P2447 [SDOI2010]外星千足蟲

模板提交處:P2455 [SDOI2006]線性方程組; P3389 【模板】高斯消元法

實數模板:

for (register int i = 1; i <= n; ++i) {//消去第i列的元 
	int mx = i;//用第mx行來消元 
	for (register int j = i + 1; j <= n; ++j)//前i行任務已經完成,剩下的為可用行 
		if (F(a[j][i]) > F(a[mx][i]))	mx = j;//防止選第i個元係數為0的方程來消元 
	if (F(a[mx][i]) <= eps)	failed();//第i個元係數全為0,方程有無數解 
	for (register int j = 1; j <= n; ++j) {//去消掉第j行的第i元係數 
		if (j == mx)	continue;
		double k = a[j][i] / a[mx][i];
		for (register int p = 1; p <= n + 1; ++p)
			a[j][p] -= a[mx][p] * k;
	}
	for (register int p = 1; p <= n + 1; ++p)//完成任務,放到前面 
		swap(a[mx][p], a[i][p]);
}
//x[i] = a[i][n + 1] / a[i][i];

實數含判斷解的情況 模板:

inline void che() {
	for (register int i = n; i; --i) {
		if (F(h[i][n + 1]) <= eps)	continue;
		bool flag = false;
		for (register int j = 1; j <= n; ++j) {
			if (F(h[i][j]) > eps) {
				flag = true; break;
			}
		}
		if (!flag) {
			puts("-1");
			exit(0);
		}
	}
	if (fg) {
		puts("0");
		exit(0);
	}
}

inline gx() {
	for (register int j = 1; j <= n; ++j) {
		int id = 0;
		for (register int i = j - fg; i <= n; ++i) {
			if (F(h[i][j]) <= eps)	continue;
			id = i; break;
		}
		if (!id) {
			fg++; continue;
		}
		for (register int i = 1; i <= n; ++i) {
			if (i == id)	continue;
			double k = h[i][j] / h[id][j];
			for (register int lie = j; lie <= n + 1; ++lie) {
				h[i][lie] -= k * h[id][lie];
			}
		}
		swap(h[j - fg], h[id]);
	}
}

取模模板:

//m = n + 1
for (register int i = 1; i <= n; ++i) {
	for (register int j = i; j <= n; ++j) 
		if (a[j][i]) { swap(a[j], a[i]); break; }
	if (!a[i][i])	failed();
	ll inv = get_inv(a[i][i]);
	for (register int p = i; p <= m; ++p)	a[i][p] = a[i][p] * inv % P;
    //把改行乘inv,將a[i][i]變為1
	for (register int j = 1; j <= n; ++j) {
		if (j == i)	continue;
		ll k = a[j][i];
		for (register int p = i; p <= m; ++p)
			a[j][p] = ((a[j][p] - a[i][p] * k) % P + P) % P;
	}
}
//左邊n*n矩陣直接消成單位矩陣
//x[i] = a[i][n + 1];

矩陣求逆

ryc學長課件:

求逆矩陣

對於一個矩陣An×n,先構造一個增廣矩陣W=[A∣E] ,其中E 是一個n×n的單位矩陣,這樣W就成了一個n×2n的矩陣。
對W進行行變換,使之變成[E∣B] 的形式,這樣就可以確定A^(−1)=B。

證明如下:因為是僅對增廣矩陣W做行變換,因此,其做變換的過程可看為對W 左乘一個可逆矩陣P,使之滿足PW=P[A∣E]=[PA∣P]=[E∣B] ,因此,有P=B且PA=E,顯然,就有BA=E,則矩陣B就為矩陣A的逆。

挑重點:

對於一個矩陣A (n×n),先構造一個增廣矩陣W=[A∣E] ,其中E 是一個n×n的單位矩陣,這樣W就成了一個n×2n的矩陣。

對W進行行變換,使之變成[E∣B] 的形式,這樣就可以確定A^(−1)=B。

複合矩陣

把矩陣當作元素的矩陣,即矩陣套矩陣

其中 \(0\) 為全0的矩陣, \(1\) 為對角線(左上到右下)為1,其它為0的對角矩陣。

支援矩陣快速冪等。

注意複合矩陣的矩陣乘法要先給初始的那個 \(Matrix\) 初始化,即賦值n, m,不然小矩陣的乘法會出錯。

例題:UVA11149 Power of Matrix

求行列式&基爾霍夫矩陣樹定理

詳見:矩陣樹定理(+行列式)

實用版:lhm_

例題:P4111 [HEOI2015]小 Z 的房間

  • 求行列式

首先要知道行列式的性質:

  1. 轉置後行列式不變

  2. 交換兩行後行列式為相反數

  3. 一行乘某個係數再加到另一行,行列式不變。

...(基本就用到這麼多了)

因此我們需要一種類似高斯消元的演算法。

模意義下求行列式時,可以用輾轉相除法:用下面的行消去上面的行,然後把上面的行和下面的行互換,並且ans×=-1,直到下面的行的某係數為0為止。由於是求行列式,我們最終消成上三角矩陣即可,即每次只用i行去和下面的行消即可。

由於是輾轉相除法,就不用再找該列最大數了,因為如果用的是個0的話,會被交換到下面去。

  • 基爾霍夫矩陣樹定理

無向圖生成樹(把所有點全部用上)的方案數 = |度數矩陣 - 鄰接矩陣|(行列式)

實際上,如果生成樹帶邊權,一棵生成樹的權值為邊權積,那麼所有的生成樹的權值和其實還是 |度數矩陣 - 鄰接矩陣|,只不過都要乘上個權值。

如果求有向圖的生成樹的權值和,我們也是可以做的,不過還要分外向樹和內向樹。外向樹的度數矩陣是入度矩陣,內向樹的度數矩陣是出度矩陣,並且要刪去的那一行一列要恰好是根的那一行一列

其中要把矩陣的最後一行和最後一列刪去

inline void add(int x, int y) {//x和y連一條無向邊
	a[x][x]++; a[y][y]++;//度數矩陣 
	a[x][y]--; a[y][x]--;//鄰接矩陣
}


inline ll gx(int n) {
	ll ans = 1;
	for (register int i = 1; i <= n; ++i) {
		for (register int j = i + 1; j <= n; ++j) {
			while (a[j][i]) {
				ll k = a[i][i] / a[j][i];
				for (register int p = 1; p <= n; ++p) 
					a[i][p] = ((a[i][p] - k * a[j][p]) % P + P) % P, swap(a[i][p], a[j][p]);
				ans *= -1;
			}
		}
		if (a[i][i] == 0)	return 0;
		ans = ans * a[i][i] % P;
	}
	return (ans + P) % P;
}

int main() {
	...
   //連邊,tot為總點數
	printf("%lld\n", gx(tot - 1));
	return 0;
}

線性基

二進位制版線性基

實數版線性基

例題:P3265 [JLOI2015]裝備購買

這回要來真的線性基了。

我們發現,線性基的數量其實就是矩陣的秩。因此我們用高斯消元求出矩陣的秩就可以了。

具體地說,我們還是開一個數組d存第i個元素的線性基是第幾行。當我們發現某個元素還沒有其對應的線性基時,我們就把線性基設定成它,統計答案並直接退出該行。如果已經有線性基,就用線性基的那一行消去該行的那個元素,這樣這行在那個元素方面就肯定與線性基那行線性無關了。

特別地,如果那個元素為0,就不用管它了,因為它既不能當線性基,又不能與該元素的線性基那行線性相關。就直接進行下一個元素即可。

其實就相當於消成類似上三角矩陣(雖然遇到0就不是了,但是能保證當選線性基的那個元素的左邊都是0)。

一行可以被上面的行線性表出的話,這一行最後應該全被消成0

for (register int i = 1; i <= n; ++i) {
	for (register int p = 1; p <= m; ++p) {
		if (F(nod[i].a[p]) <= eps) continue;
		if (!d[p]) {
			d[p] = i;
			ans += nod[i].fare;
			cnt++;
			break;
		}
		double k = nod[i].a[p] / nod[d[p]].a[p];
		for (register int np = p; np <= m; ++np) 
			nod[i].a[np] -= k * nod[d[p]].a[np];
	}
}

矩陣乘法,矩陣快速冪(除錯用)

struct matrix {
	int h[N][N];
	int n, m;
	matrix() {
		memset(h, 0, sizeof(h));
	}
	void printm() {//debug 
		puts("print:");
		for (register int i = 1; i <= n; ++i) {
			for (register int j = 1; j <= n; ++j) {
				printf("%d ", h[i][j]);
			}
			puts("");
		}
		puts("");
	}
	matrix operator +(const matrix a) const {
		matrix mx; mx.n = n; mx.m = m;
		for (register int i = 1; i <= n; ++i) {
			for (register int j = 1; j <= m; ++j) {
				mx.h[i][j] = (h[i][j] + a.h[i][j]) % P;
			}
		}
		return mx;
	}
	matrix operator *(const matrix a) const {
		matrix mx; mx.n = n; mx.m = a.m;
		for (register int k = 1; k <= n; ++k) {
			for (register int i = 1; i <= m; ++i) {
				for (register int j = 1; j <= a.m; ++j) {
					mx.h[i][j] = (mx.h[i][j] + h[i][k] * a.h[k][j]) % P;
				}
			}
		}
		return mx;
	}
};
inline matrix quickpow(fuhe x, int k) {
	--k;
	matrix res = x;
	while (k) {
		if (k & 1)	res = res * x;
		x = x * x;
		k >>= 1;
	}
	return res;
}

高斯消元模板(除錯用)

實數簡化版

for (register int i = 1; i <= n; ++i) {//消去第i列的元 
    int mx = i;//用第mx行來消元 
    for (register int j = i + 1; j <= n; ++j)//前i行任務已經完成,剩下的為可用行 
        if (F(a[j][i]) > F(a[mx][i]))   mx = j;//防止選第i個元係數為0的方程來消元 
    if (F(a[mx][i]) <= eps) failed();//第i個元係數全為0,方程有無數解 
    for (register int j = 1; j <= n; ++j) {//去消掉第j行的第i元係數 
        if (j == mx)    continue;
        double k = a[j][i] / a[mx][i];
        for (register int p = 1; p <= n + 1; ++p)
            a[j][p] -= a[mx][p] * k;
    }
    for (register int p = 1; p <= n + 1; ++p)//完成任務,放到前面 
        swap(a[mx][p], a[i][p]);
}
//x[i] = a[i][n + 1] / a[i][i];

實數含特判解情況版:

inline void che() {
    for (register int i = n; i; --i) {
        if (F(h[i][n + 1]) <= eps)  continue;
        bool flag = false;
        for (register int j = 1; j <= n; ++j) {
            if (F(h[i][j]) > eps) {
                flag = true; break;
            }
        }
        if (!flag) {
            puts("-1");//無解
            exit(0);
        }
    }
    if (fg) {
        puts("0");//有無陣列解
        exit(0);
    }
}

inline gx() {
    for (register int j = 1; j <= n; ++j) {
        int id = 0;
        for (register int i = j - fg; i <= n; ++i) {
            if (F(h[i][j]) <= eps)  continue;
            id = i; break;
        }
        if (!id) {
            fg++; continue;
        }
        for (register int i = 1; i <= n; ++i) {
            if (i == id)    continue;
            double k = h[i][j] / h[id][j];
            for (register int lie = j; lie <= n + 1; ++lie) {
                h[i][lie] -= k * h[id][lie];
            }
        }
        swap(h[j - fg], h[id]);
    }
}

取模版

//m = n + 1
for (register int i = 1; i <= n; ++i) {
    for (register int j = i; j <= n; ++j) 
        if (a[j][i]) { swap(a[j], a[i]); break; }
    if (!a[i][i])   failed();
    ll inv = get_inv(a[i][i]);
    for (register int p = i; p <= m; ++p)   a[i][p] = a[i][p] * inv % P;
    //把改行乘inv,將a[i][i]變為1
    for (register int j = 1; j <= n; ++j) {
        if (j == i) continue;
        ll k = a[j][i];
        for (register int p = i; p <= m; ++p)
            a[j][p] = ((a[j][p] - a[i][p] * k) % P + P) % P;
    }
}
//左邊n*n矩陣直接消成單位矩陣
//x[i] = a[i][n + 1];

基爾霍夫矩陣樹定理 + 求行列式

輾轉相減法

inline void add(int x, int y) {//x和y連一條無向邊
    a[x][x]++; a[y][y]++;//度數矩陣 
    a[x][y]--; a[y][x]--;//鄰接矩陣
}

inline ll gx(int n) {
    ll ans = 1;
    for (register int i = 1; i <= n; ++i) {
        for (register int j = i + 1; j <= n; ++j) {
            while (a[j][i]) {
                ll k = a[i][i] / a[j][i];
                for (register int p = 1; p <= n; ++p) 
                    a[i][p] = ((a[i][p] - k * a[j][p]) % P + P) % P, swap(a[i][p], a[j][p]);
                ans *= -1;
            }
        }
        if (a[i][i] == 0)   return 0;
        ans = ans * a[i][i] % P;
    }
    return (ans + P) % P;
}

int main() {
    ...
   //連邊,tot為總點數
    printf("%lld\n", gx(tot - 1));
    return 0;
}

逆元法

int n, m, t;
int h[N][N];
inline void gx() {
	ll ans = 1;
	for (register int i = 1; i <= n; ++i) {
		int id = -1;
		for (register int hang = i; hang <= n; ++hang) {
			if (h[hang][i] != 0) {
				id = hang; break;
			}
		}
		if (id == -1) {
			puts("0");
			return ;
		}
		ll inv = quickpow(h[id][i], P - 2);//Attention!!
		ans = ans * h[id][i] % P;
		for (register int j = i; j <= n; ++j)	h[id][j] = 1ll * h[id][j] * inv % P;
		for (register int hang = i; hang <= n; ++hang) if (hang != id) {
			ll k = h[hang][i];
			for (register int j = i; j <= n; ++j) {
				DEL(h[hang][j], k * h[id][j] % P);
			}
		}
		if (id != i) {
			ans = P - ans;
			swap(h[id], h[i]);
		}
	}
	ans %= P;
	if (ans < 0)	ans += P;
	printf("%lld\n", ans);
}

namespace jzp1 {//無向圖求生成樹
	inline void addedge(int u, int v, int val) {
		ADD(h[u][u], val), ADD(h[v][v], val);
		DEL(h[u][v], val), DEL(h[v][u], val);
	}
	
	inline void sol() {
		for (register int i = 1; i <= m; ++i) {
			int u, v, w; read(u), read(v), read(w);
			addedge(u, v, w);
		}
		--n;
		gx();
	}
}

namespace jzp2 {//有向圖求外向樹
	inline void Read(int &cur) {
		read(cur);
		--cur;
		if (!cur)	cur = n;
	}
	inline void addedge(int u, int v, int val) {
		DEL(h[u][v], val);
		ADD(h[v][v], val);
	}
	inline void sol() {
		for (register int i = 1; i <= m; ++i) {
			int u, v, w; Read(u), Read(v), read(w);
			if (u != v) addedge(u, v, w);
		}
		--n;
		gx();
	}
}