【Learning】矩陣樹定理 Matrix-Tree


矩陣樹定理 Matrix Tree

  
​  矩陣樹定理主要用於圖的生成樹計數。
  
  看到給出圖求生成樹的這類問題就大概要往這方面想了。
  
  算法會根據圖構造出一個特殊的基爾霍夫矩陣\(A\),接着根據矩陣樹定理,用\(A\)計算出生成樹個數。
  
  
  

1.無向圖的生成樹計數

  
  對於給定的可含重邊的連通無向圖\(G\),求其生成樹的個數。求法如下:
  
  定義度數矩陣\(D\):該矩陣僅在對角線上有值,\(D_{i,i}\)表示\(i\)號點的度數。對於圖中每一條無向邊\((u,v)\)\(D_{u,u}\)++,\(D_{v,v}\)++。
  
  定義鄰接矩陣\(C\)\(C_{i,j}\)表示\(i\)\(j\)的邊數。對於圖中每一條無向邊\((u,v)\)\(C_{u,v}\)++,\(C_{v,u}\)++。
  
  定義圖\(G\)基爾霍夫矩陣\(A=D-C\)
  
​  矩陣樹定理:將\(A\)去掉第\(i\)行和第\(i\)列(\(i\in[1,n]\)),將它當做一個行列式求解,則\(\det(A)\)就是生成樹個數。
  
    
  

2.有向圖的樹形圖計數

  
  對於有向圖,不存在“生成樹”的概念,但存在“樹形圖”的概念。有向圖中,若選定一個點作為樹根,能構造出一棵“樹”(包含\(n-1\)條邊)使得根能到達任意節點,則這是一棵外向樹;若能構造出一棵“樹”使得任意節點能到達根,則這是一棵內向樹。
  
  定義度數矩陣\(D\):該矩陣僅在對角線上有值,\(D_{i,i}\)表示\(i\)號點的度數。
  
           對於圖中每一條有向邊\((u,v)\),若構造外向樹則\(D_{v,v}\)++;若構造內向樹則\(D_{u,u}\)++。
  
  定義鄰接矩陣\(C\)\(C_{i,j}\)表示\(i\)\(j\)的邊數。對於圖中每一條有向邊\((u,v)\)\(C_{u,v}\)++。
  
​  定義圖\(G\)基爾霍夫矩陣\(A=D-C\)
  
  矩陣樹定理:將\(A\)去掉第\(i\)行和第\(i\)列(\(i\in[1,n]\)),將它當做一個行列式求解,則\(\det(A)\)就是以\(i\)為根的外向/內向樹形圖個數。很多時候我們會發現\(A\)的對角線上某數為\(A_{i,i}=0\),刪去第\(i\)行和第\(i\)列可以干掉0。只有這樣行列式才不等於0,其實也就是說只能從\(i\)出發有解了。
  
   
  

3.細節

  
  求行列式的方法是:將行列式通過行列式初等變換消成上三角。此時對角線乘積即為行列式的值。
  
  注意,矩陣樹定理這一套算法會考慮如何把所有的參與點構建成生成樹,所以編號不要跳躍。如果說有障礙之類的元素,千萬不要在矩陣中給它留一行一列,因為這一行一列都一定是0,算法會嘗試將“障礙”構建進生成樹,最后只能得到無解。
  
  
  
  
  

一些例題

  

BZOJ 4031

  
  傳送門在此
  
  這是一道無向圖生成樹計數的裸題,直接上基礎算法即可。
  
  這里就要注意障礙的處理了。我們應該對非障礙格子重新標號使得它們的編號連續,保證算法正常進行。
  
  這題的模數真的惡心,不是質數,沒法用逆元。所以使用類輾轉相除法來消元,每行的消元從\(O(n)\)變成\(O(n\lg )\)
  

#include <cstdio>
using namespace std;
const int N=10,MOD=1e9;
int n,m,id[N][N],idcnt,a[N*N][N*N];
char map[N][N];
inline void swap(int &x,int &y){x^=y^=x^=y;}
inline int plus(int x,int y){return (x+y)%MOD;}
inline int mul(int x,int y){return 1LL*x*y%MOD;}
inline bool ok(int x,int y){return 1<=x&&x<=n&&1<=y&&y<=m&&map[x][y]=='.';}
void addEdge(int u,int v){
    a[u][u]++; a[v][v]++;
    a[u][v]--; a[v][u]--;
}
int solve(){
    if(idcnt==1) return 1;
    int all=idcnt-1,res=1;
    for(int i=1;i<=all;i++)
        for(int j=i+1;j<=all;j++)
            while(a[j][i]){
                int t=a[i][i]/a[j][i];
                for(int k=i;k<=all;k++){
                    int q=plus(a[i][k],-mul(a[j][k],t));
                    a[i][k]=a[j][k];
                    a[j][k]=q;
                }
                res=-res;
            }
    for(int i=1;i<=all;i++) res=mul(res,a[i][i]);
    return (res+MOD)%MOD;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%s",map[i]+1);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(map[i][j]=='.') id[i][j]=++idcnt;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(ok(i,j)){
                if(ok(i+1,j))
                    addEdge(id[i][j],id[i+1][j]);
                if(ok(i,j+1))
                    addEdge(id[i][j],id[i][j+1]);
            }
    printf("%d\n",solve());
    return 0;
}

  
  
  
  
  

BZOJ 4894

  
  傳送門
  
  這是一道有向圖樹形圖計數。要求以1號點為根的外向樹形圖個數。
  
  按照上述做法直接寫即可。刪去\(A\)的第1行第1列,因為1號點沒有入邊,若不刪第一行第一列行列式值為0,無法計算。
  

#include <cstdio>
using namespace std;
const int N=305,MOD=1e9+7;
int n,a[N][N];
char str[N];
inline int mul(int x,int y){return 1LL*x*y%MOD;}
inline int plus(int x,int y){return (x+y)%MOD;}
inline void swap(int &x,int &y){x^=y^=x^=y;}
int ksm(int x,int y){
    int res=1;
    for(;y;x=mul(x,x),y>>=1)
        if(y&1) res=mul(res,x);
    return res;
}
int gaussian(){
    int res=1,size=n-1;
    for(int i=1;i<size;i++){
        if(!a[i][i]){
            int l;
            for(l=i+1;l<=size;l++)
                if(a[l][i]) break;
            if(l<=size&&a[l][i]){
                for(int j=i;j<=size;j++) swap(a[l][j],a[i][j]);
                res=-res;
            }
            else return 0;
        }
        for(int j=i+1;j<=size;j++){
            int t=mul(a[j][i],ksm(a[i][i],MOD-2));
            for(int k=i;k<=size;k++)         
                a[j][k]=plus(a[j][k],-mul(a[i][k],t));
        }
    }
    for(int i=1;i<=size;i++) res=mul(res,a[i][i]);
    return plus(res,MOD);
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%s",str+1);
        for(int j=1;j<=n;j++)
            if(str[j]=='1') 
                a[j][j]++,a[i][j]--;
    }
    for(int i=1;i<n;i++)
        for(int j=1;j<n;j++) a[i][j]=a[i+1][j+1];
    printf("%d\n",gaussian());
    return 0;
}

  
  
  
  
  

BZOJ 4596

  
  傳送門
  
​  我開始不會做了。
  
​  我們發現如果將所有公司提供的邊都加進圖中,然后求生成樹個數,是無法限制“每個公司至少要建一條”這個條件的。有的生成樹可能只有一部分公司參與,比如說某一種生成樹只含有\(S\)集合的公司。
  
  如果僅加入\(S\)集合所含公司的邊,我們發現這些答案也會被統計到。既然有重復統計,可以考慮消除嗎?
  
​  於是容斥的思想就體現出來了!
  
  我們枚舉加入哪一些公司,分別求生成樹個數。記參與公司集合為\(S\)時生成樹個數為\(f(S)\),記有\(x\)個公司參與時的生成樹總方案為\(中有個A_x=\sum_{S中有x個}f(S)\),則

\[Ans=A_n-A_{n-1}+A_{n-2}-A_{n-3}...... \]

​  復雜度為\(O(2^nn^3)\),其實是可以跑的過的。
  

#include <cstdio>
#include <vector>
#define mp make_pair
#define pb push_back
using namespace std;
typedef pair<int,int> pii;
const int N=18,MOD=1e9+7;
int n;
int a[N][N];
vector<pii> l[N];
vector<int> b[N];
inline bool in(int i,int j){return (i>>(j-1))&1;}
inline int plus(int x,int y){return (x+y)%MOD;}
inline int mul(int x,int y){return 1LL*x*y%MOD;}
inline void swap(int &x,int &y){x^=y^=x^=y;}
void clear_mat(){
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++) a[i][j]=0;
}
void add_company(int id){
	for(int i=0,sz=l[id].size();i<sz;i++){
		int u=l[id][i].first,v=l[id][i].second;
		a[u][u]++; a[v][v]++;
		a[u][v]--; a[v][u]--;
	}
}
int ksm(int x,int y){
	int res=1;
	for(;y;x=mul(x,x),y>>=1)
		if(y&1) res=mul(res,x);
	return res;
}
int gaussian(){
	int s=n-1,res=1;
	for(int i=1;i<=s;i++){
		if(!a[i][i]){
			int t;
			for(t=i+1;t<=s&&!a[t][i];t++);
			if(t>s) return 0;
			for(int j=i;j<=s;j++) swap(a[i][j],a[t][j]);
			res=-res;
		}
		int inv=ksm(a[i][i],MOD-2);
		for(int j=i+1;j<=s;j++){
			int t=mul(a[j][i],inv);
			for(int k=i;k<=s;k++)
				a[j][k]=plus(a[j][k],-mul(a[i][k],t));
		}
	}
	for(int i=1;i<=s;i++) res=mul(res,a[i][i]);
	return res;
}
int main(){
	scanf("%d",&n);
	for(int i=1,m;i<n;i++){
		scanf("%d",&m);
		for(int j=1,u,v;j<=m;j++){
			scanf("%d%d",&u,&v);
			l[i].pb(mp(u,v));	
		}
	}
	int all=1<<(n-1);
	for(int i=1;i<all;i++){
		int cnt=0;
		for(int j=1;j<=n;j++) 
			cnt+=in(i,j);
		b[cnt].pb(i);	
	}
	int ans=0;
	for(int i=n-1,r=1;i>=1;i--,r=-r)
		for(int j=0,sz=b[i].size();j<sz;j++){
			clear_mat();
			int st=b[i][j];
			for(int c=1;c<n;c++)
				if(in(st,c)) add_company(c);
			ans=plus(ans,gaussian()*r);				
		}
	ans=plus(ans,MOD);
	printf("%d\n",ans);
	return 0;
}

  
  
  

總結

  
  矩陣樹定理本身還是挺簡單的,但願自己不要忘得太快......
  
  但是要靈活運用(廢話)
  
​  如果要深入透徹,我還是得研究一下矩陣樹定理的證明。不過就當一個大坑先留着吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM