一篇自己都看不懂的Matrix tree學習筆記
Matrix tree定理用於連通圖生成樹計數,由於博主太菜看不懂定理證明,所以本篇部落格不提供\(Matrix\ tree\)定理的證明內容(反正這個東西背結論就可以了是吧)
理解\(Matrix\ tree\)定理需要一定的線性代數知識(當然不會也沒關係)
a.前置芝士——行列式
稍微費點筆墨寫寫行列式
行列式是一個\(N \times N\)的方陣,比如說下面就是一個\(3 \times 3\)的行列式
\(\left|\begin{array}{cccc} 1 & 6 & 9 \\ 7 & 0 & 7\\ 9 & 4 & 3 \end{array}\right|\)
下面我們統一某一個矩陣第\(i\)行第\(j\)列的元素為\(a_{i,j}\)
當然,行列式相比於矩陣的不同之處在於:行列式雖然長成了一個矩陣的樣子,但它代表的是一個值,這個值的定義為\(\sum\limits_p s(p) \times \sum\limits_{i=1}^N a_{i,p_i}\),其中\(p\)是一個排列,\(s(p)=-1^{f(p)}\),\(f(p)\)等於排列\(p\)的逆序對數。
行列式有幾個性質:
\(1.\)交換行列式的兩行或者兩列,行列式的值取相反數
這裡(偽)證明交換兩行值取反,交換兩列同理可得到。
設變換前的行列式為\(a_{i,j}\)
可以知道對\(a_{i,j}\)求值時所有的列舉的排列\(p\),在\(b_{i,j}\)求值時一定存在一個排列\(q\),使得\(\forall i , a_{i,p_i} = b_{i,q_i}\),而排列\(q\)顯然是排列\(p\)交換第\(m\)個和第\(n\)個元素得來的,在排列中交換一次兩個元素逆序對改變奇偶性,所以對於所有的\(p,q\),\(s(q)=-s(p)\),QED
\(2.\)給行列式的一行或一列的所有數同時乘上同一個值,行列式的值也會乘上這一個值
這個直接代入定義即可
\(3.\)將一行列式的某一行(列)乘若干倍加到另一行(列)上,行列式的值不變
直接帶入定義……其實是我懶得寫了
說上面這些性質有什麼用捏……
話說定義中行列式的求值是\(O(n \times n!)\)的,有沒有多項式做法呢……
其實我們可以使用類似於高斯消元的方法用\(O(n^3)\)的複雜度解決這個問題
可以發現我們在高斯消元中做的所有操作在行列式性質中都有其值的變化
那麼我們將其通過與高斯消元相同的方法將行列式變為上三角型(也就是\(i>j \rightarrow a_{i,j}=0\)的行列式),在外部記錄當前行列式的正負,此時在定義計算式中能產生貢獻的只有\(1,2,3...n\)這一個排列,對應行列式對角線上的值的乘積,乘起來然後判號即可。
b.\(Matrix\ tree\)定理內容
對於一張連通無向圖,定義其度數矩陣為矩陣\(a_{i,j}\),其中\(a_{i,j} = \left\{\begin{array}{rcl} d_i, & i=j \\ 0, & i \neq j \end{array}\right.\),用度數矩陣減去鄰接矩陣得到基爾霍夫矩陣,劃去基爾霍夫矩陣的第\(x\)行與第\(y\)列(\(x,y\)任意選定)得到一個新的行列式,這個行列式的值乘上\((-1)^{x+y}\)就是這個無向圖的生成樹數量。當然了為了方便一般就劃掉第\(1\)行第\(1\)列或者第\(N\)行第\(N\)列。
這種東西是不可能會證的qaq
c.一些裸題
HEOI2015 小Z的房間
板子題
#include<bits/stdc++.h>
#define P pair < int , int >
#define ind(i,j) (node[P(i,j)])
#define int long long
//This code is written by Itst
using namespace std;
const int MOD = 1e9;
char room[11][11];
int N , M , mat[82][82];
map < P , int > node;
inline void calc(int x , int y , int &a , int &b , int &c , int &d , int &f){
a = d = f = 1;
b = c = 0;
while(y){
int t = x / y;
x -= t * y;
a = (a - t * c % MOD + MOD) % MOD;
b = (b - t * d % MOD + MOD) % MOD;
swap(x , y);
swap(a , c);
swap(b , d);
f *= -1;
}
}
signed main(){
cin >> N >> M;
int cnt = 0;
for(int i = 1 ; i <= N ; ++i)
for(int j = 1 ; j <= M ; ++j){
cin >> room[i][j];
if(room[i][j] == '.'){
node[P(i,j)] = ++cnt;
if(room[i - 1][j] == '.'){
mat[ind(i - 1 , j)][ind(i , j)] = mat[ind(i , j)][ind(i - 1 , j)] = MOD - 1;
++mat[ind(i , j)][ind(i , j)];
++mat[ind(i - 1 , j)][ind(i - 1 , j)];
}
if(room[i][j - 1] == '.'){
mat[ind(i , j - 1)][ind(i , j)] = mat[ind(i , j)][ind(i , j - 1)] = MOD - 1;
++mat[ind(i , j)][ind(i , j)];
++mat[ind(i , j - 1)][ind(i , j - 1)];
}
}
}
int ans = 1;
for(int i = 1 ; i < cnt ; ++i)
for(int j = i + 1 ; j < cnt ; ++j){
if(mat[j][i]){
int a , b , c , d , f;
calc(mat[i][i] , mat[j][i] , a , b , c , d , f);
ans = (MOD + ans * f) % MOD;
for(int k = i ; k < N * M ; ++k){
int p = (mat[i][k] * a + mat[j][k] * b) % MOD;
int q = (mat[i][k] * c + mat[j][k] * d) % MOD;
mat[i][k] = p;
mat[j][k] = q;
}
}
}
for(int i = 1 ; i < cnt ; ++i)
ans = ans * mat[i][i] % MOD;
cout << ans;
return 0;
}
JSOI2008 最小生成樹計數
先擼出一棵最小生成樹出來,對於邊權相等的邊統一計算。在計算邊權為某一個值的答案時,將最小生成樹上其他的邊連起來,將一個連通塊看作一個點,將這一些邊權相等的邊與這些點看作新圖,在新圖上跑生成樹計數,所有邊權的答案的乘積就是最後答案
注意到這道題的模數比較NB,並不是一個質數,這意味著不能乘逆元。我們可以用類似輾轉相除法的思路進行計算。對於當前需要減的兩行,通過輾轉相除計算係數與交換行的次數,然後再帶入這兩行中。完全講不清楚
#include<bits/stdc++.h>
//This code is written by Itst
using namespace std;
inline int read(){
int a = 0;
char c = getchar();
while(!isdigit(c))
c = getchar();
while(isdigit(c)){
a = (a << 3) + (a << 1) + (c ^ '0');
c = getchar();
}
return a;
}
const int MOD = 31011;
struct Edge{
int s , t , w;
bool operator <(const Edge a)const{
return w < a.w;
}
}Ed[10010];
int mat[11][11] , fa[110] , N , M , cntL , ans = 1;
map < int , int > lsh;
bool vis[10010];
int find(int x){
return fa[x] == x ? x : (fa[x] = find(fa[x]));
}
inline int getL(int x){
if(!lsh.count(x))
lsh[x] = ++cntL;
return lsh[x];
}
//輾轉相除
inline void calc(int x , int y , int &a , int &b , int &c , int &d , int &flg){
a = d = flg = 1;
c = b = 0;
while(y){
int t = x / y;
x -= t * y;
a = (a - t * c % MOD + MOD) % MOD;
b = (b - t * d % MOD + MOD) % MOD;
swap(x , y);
swap(a , c);
swap(b , d);
flg *= -1;
}
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in" , "r" , stdin);
//freopen("out" , "w" , stdout);
#endif
N = read();
M = read();
for(int i = 1 ; i <= M ; ++i){
Ed[i].s = read();
Ed[i].t = read();
Ed[i].w = read();
}
for(int i = 1 ; i <= N ; ++i)
fa[i] = i;
sort(Ed + 1 , Ed + M + 1);
for(int i = 1 ; i <= M ; ++i)
if(fa[find(Ed[i].s)] != fa[find(Ed[i].t)]){
fa[find(Ed[i].s)] = find(Ed[i].t);
vis[i] = 1;
}
for(int i = 2 ; i <= N ; ++i)
if(find(i) != find(1)){
puts("0");
return 0;
}
for(int i = 1 ; i <= M ; ){
int p = i;
cntL = 0;
lsh.clear();
memset(mat , 0 , sizeof(mat));
for(int j = 1 ; j <= N ; ++j)
fa[j] = j;
for(int j = 1 ; j <= M ; ++j)
if(vis[j] && Ed[j].w != Ed[i].w)
fa[find(Ed[j].s)] = find(Ed[j].t);
while(p <= M && Ed[i].w == Ed[p].w){
int m = find(Ed[p].s) , n = find(Ed[p].t);
if(m != n){
m = getL(m);
n = getL(n);
--mat[m][n];
--mat[n][m];
++mat[m][m];
++mat[n][n];
}
++p;
}
for(int j = 1 ; j < cntL ; ++j)
for(int k = 1 ; k < cntL ; ++k)
if(mat[j][k] < 0)
mat[j][k] += MOD;
for(int j = 1 ; j < cntL ; ++j)
for(int k = j + 1 ; k < cntL ; ++k)
if(mat[k][j]){
int a , b , c , d , flg;
calc(mat[j][j] , mat[k][j] , a , b , c , d , flg);
ans = (ans * flg + MOD) % MOD;
for(int l = j ; l < cntL ; ++l){
int r = (mat[j][l] * a + mat[k][l] * b) % MOD;
int s = (mat[j][l] * c + mat[k][l] * d) % MOD;
mat[j][l] = r;
mat[k][l] = s;
}//帶入輾轉相除計算出來的係數
}
for(int j = 1 ; j < cntL ; ++j)
if(mat[j][j])
ans = (ans * mat[j][j] % MOD + MOD) % MOD;
while(i < p){
fa[find(Ed[i].s)] = find(Ed[i].t);
++i;
}
}
cout << ans << endl;
return 0;
}
注意必須要將最小生成樹上其他的邊連起來,因為對於不連通的圖矩陣行列式的值會等於\(0\)。
d.Matrix tree定理的一些拓展
有向圖的Matrix tree定理
對於外向生成樹,度數矩陣變為入度矩陣;對於內向生成樹,度數矩陣變為出度矩陣;如果生成樹根為\(x\),則必須劃掉第\(x\)行與第\(x\)列 ,其他與無向圖Matrix tree定理一致。
CQOI2018 社交網路
裸題
#include<bits/stdc++.h>
//This code is written by Itst
using namespace std;
inline int read(){
int a = 0;
char c = getchar();
bool f = 0;
while(!isdigit(c)){
if(c == '-')
f = 1;
c = getchar();
}
while(isdigit(c)){
a = (a << 3) + (a << 1) + (c ^ '0');
c = getchar();
}
return f ? -a : a;
}
const int MOD = 1e4 + 7;
int mat[251][251];
inline int poww(int a , int b){
int times = 1;
while(b){
if(b & 1)
times = times * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return times;
}
int main(){
int N = read() , M = read();
for(int i = 1 ; i <= M ; ++i){
int b = read() , a = read();
--mat[a][b];
++mat[b][b];
}
for(int i = 2 ; i <= N ; ++i)
for(int j = 2 ; j <= N ; ++j)
if(!mat[i][j])
mat[i][j] += MOD;
int ans = 1;
for(int i = 2 ; i <= N ; ++i){
for(int j = i ; j <= N ; ++j)
if(mat[j][i]){
swap(mat[i] , mat[j]);
if(i != j)
ans = MOD - ans;
break;
}
int t = poww(mat[i][i] , MOD - 2);
for(int j = i + 1 ; j <= N ; ++j)
if(mat[j][i])
for(int k = N ; k >= i ; --k)
mat[j][k] = (mat[j][k] - mat[i][k] * t % MOD * mat[j][i] % MOD + MOD) % MOD;
}
for(int i = 2 ; i <= N ; ++i)
ans = ans * mat[i][i] % MOD;
cout << ans;
return 0;
}
無向圖所有生成樹的邊權乘積的和
考慮如果邊權都是整數,可以把邊權為\(w\)的邊看成\(w\)條重邊,可以直接用定理,有理數每一行乘上分母的\(lcm\)將所有邊權都乘成整數然後最後將答案除掉\(lcm^{N-1}\),那麼Matrix tree定理顯然也是正確的
SDOI2014 重建
設題目給出的邊集為\(E\),某一棵生成樹的邊集為\(e\)
那麼這一棵生成樹的貢獻是\(\prod\limits_{(u,v) \in e} G_{u,v} \times \prod\limits_{(u,v) \in E \&\& (u,v) \not\in e}(1-G_{u,v})=\prod\limits_{(u,v) \in E} (1 - G_{u,v}) \times \prod\limits_{(u,v) \in e} \frac{G_{u,v}}{1 - G_{u,v}}\)
然後套上板子就可以了
#include<bits/stdc++.h>
#define ld long double
#define eps 1e-8
//This code is written by Itst
using namespace std;
ld mat[51][51];
int N;
bool cmp(ld a , ld b){
return a + eps > b && a - eps < b;
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in" , "r" , stdin);
//freopen("out" , "w" , stdout);
#endif
cin >> N;
for(int i = 1 ; i <= N ; ++i)
for(int j = 1 ; j <= N ; ++j)
cin >> mat[i][j];
ld base = 1;
for(int i = 1 ; i <= N ; ++i)
for(int j = i + 1 ; j <= N ; ++j){
if(cmp(mat[i][j] , 1))
mat[i][j] = 1 - eps;
base *= (1 - mat[i][j]);
}
for(int i = 1 ; i <= N ; ++i){
for(int j = 1 ; j <= N ; ++j)
if(i != j){
if(cmp(mat[i][j] , 1))
mat[i][j] = 1 - eps;
mat[i][j] = mat[i][j] / (1 - mat[i][j]);
mat[i][i] += mat[i][j];
}
for(int j = 1 ; j <= N ; ++j)
if(i != j)
mat[i][j] = -mat[i][j];
}
ld ans = 1;
for(int i = 1 ; i < N ; ++i){
for(int j = i ; j < N ; ++j)
if(!cmp(mat[i][j] , eps)){
swap(mat[i] , mat[j]);
break;
}
for(int j = i + 1 ; j < N ; ++j)
if(!cmp(mat[i][j] , eps))
for(int k = N - 1 ; k >= i ; --k)
mat[j][k] -= mat[i][k] * mat[j][i] / mat[i][i];
ans *= mat[i][i];
}
cout << fixed << setprecision(6) << ans * base;
return 0;
}