1. 程式人生 > 實用技巧 >數論之矩陣-樹定理

數論之矩陣-樹定理

定義

在圖論中,矩陣樹定理\((matrix\ tree\ theorem)\)是指,圖的生成樹數量等於調和矩陣的行列式(所以需要時間多項式計算)。

前置知識:行列式

定義

對於一個矩陣 \(A[1...n][1...n]\) ,其行列式為

\(\det(A)=\sum\limits_{P}(-1)^{\mu(P)}\prod\limits_{i=1}^nA[i][p_i]\)

(其中 \(P\)\(1 \sim n\) 的全排列, \(\mu(P)\) 為排列 \(P\) 的逆序對數)

直接按照定義計算,複雜度是 \(O(n!·n)\)

性質

\(1\)、矩陣行(列)交換,行列式取反。

\(2\)、如果兩行(列)有倍數關係(1 倍也算),那麼行列式的值為 \(0\)

\(3\)、任意一行(列)乘上任意數加到任意另一行(列)上,行列式不變。

\(4\)、矩陣行(列)所有元素同時乘以數 \(k\),行列式等比例變大。

\(5\)、單位矩陣的行列式為 \(1\),同理上三角矩陣和下三角矩陣的行列式都是對角線乘積。

這些性質主要是為高斯消元做準備。

基爾霍夫矩陣

一個圖的基爾霍夫矩陣為它的度數矩陣減掉鄰接矩陣

設圖的度數矩陣矩陣為 \(A\),鄰接矩陣為 \(B\)

對於無向圖,如果存在邊 \((x,y,z)\)

\(A[x][x]+=z,A[y][y]+=z,B[x][y]+=z,B[y][x]+=z\)

對於有向圖,如果存在邊 \((x,y,z)\)

度數矩陣:如果是外向樹,那麼 \(A[y][y]+=z\),如果是內向樹,那麼 \(A[x][x]+=z\)

鄰接矩陣:都是\(B[x][y]+=z\)

此時求出來的行列式是該圖所有生成樹中邊之積的和

特殊地,當邊權都為 \(1\) 時,去掉矩陣的任意一行和一列之後得到的餘子式的行列式是當前圖的生成樹個數

如果是有向圖,則去掉第 \(k\) 行和第 \(k\) 列之後得到的餘子式的行列式是以 \(k\) 為根時的生成樹個數

高斯消元求解

高斯消元中,我們進行的操作無非是交換某兩行,給某一行加上另一行乘上一個係數,給某一行乘上一個數

而這些操作對最終行列式的影響都可以由之前得到的性質計算

因此我們可以寫出如下的程式碼

void gsxy(rg int mmax){
	rg int cs;
	for(rg int i=2;i<=mmax;i++){
		for(rg int j=i+1;j<=mmax;j++){
			while(a[j][i]){
				cs=a[i][i]/a[j][i];
				for(rg int k=i;k<=mmax;k++){
					a[i][k]-=1LL*cs*a[j][k]%mod;
					if(a[i][k]<0) a[i][k]+=mod;
				}
				std::swap(a[i],a[j]);
				ans=-ans;
				if(ans<0) ans+=mod;
			}
		}
	}
	for(rg int i=2;i<=mmax;i++){
		ans=1LL*ans*a[i][i]%mod;
	}
}

因為模數有可能不是質數,所以採用輾轉相減法實現,複雜度多一個 \(log\)

板子

傳送門

#include<cmath>
#include<cstdio>
#include<algorithm>
#include<iostream>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int mod=1e9+7,maxn=305;
int n,m,t,a[maxn][maxn],ans=1;
int ksm(rg int ds,rg int zs){
	rg int nans=1;
	while(zs){
		if(zs&1) nans=1LL*nans*ds%mod;
		ds=1LL*ds*ds%mod;
		zs>>=1;
	}
	return nans;
}
void gsxy(){
	rg int ny,cs;
	for(rg int i=2;i<=n;i++){
		for(rg int j=i+1;j<=n;j++){
			if(!a[i][i] && a[j][i]){
				ans=-ans;
				ans+=mod;
				std::swap(a[i],a[j]);
				break;
			}
		}
		ny=ksm(a[i][i],mod-2);
		for(rg int j=i+1;j<=n;j++){
			cs=1LL*a[j][i]*ny%mod;
			for(rg int k=i;k<=n;k++){
				a[j][k]=(a[j][k]-1LL*a[i][k]*cs%mod);
				if(a[j][k]<0) a[j][k]+=mod;
			}
		}
	}
}
int main(){
	n=read(),m=read(),t=read();
	rg int aa,bb,cc;
	for(rg int i=1;i<=m;i++){
		aa=read(),bb=read(),cc=read();
		if(!t){
			a[aa][aa]+=cc;
			if(a[aa][aa]>=mod) a[aa][aa]-=mod;
			a[bb][bb]+=cc;
			if(a[bb][bb]>=mod) a[bb][bb]-=mod;
			a[aa][bb]-=cc;
			if(a[aa][bb]<0) a[aa][bb]+=mod;
			a[bb][aa]-=cc;
			if(a[bb][aa]<0) a[bb][aa]+=mod;
		} else {
			a[bb][bb]+=cc;
			if(a[bb][bb]>=mod) a[bb][bb]-=mod;
			a[aa][bb]-=cc;
			if(a[aa][bb]<0) a[aa][bb]+=mod;
		}
	}
	gsxy();
	for(rg int i=2;i<=n;i++){
		ans=1LL*ans*a[i][i]%mod;
	}
	printf("%d\n",ans%mod);
	return 0;
}

題目

小 Z 的房間

題目傳送門
對於矩陣中的每一個點,都向右和向下擴充套件

如果可以連邊就連邊,跑一遍板子就可以了

要注意的是沒有邊的節點不要計算,否則會使乘積為 \(0\)

重建

題目傳送門
要能清楚題目要求的是什麼,矩陣樹定理求出來的是什麼

\[\prod_{e\in tree} p_e\prod_{e\notin tree}1-p_e=\prod_{e\in tree}\frac{p_e}{1-p_e}\prod_{e}1-p_e \]

按照新的邊權跑一遍矩陣樹定理即可

要注意不能讓分母為零,可以給它賦一個較小的數

生成樹 && 輪狀病毒

題目傳送門1
題目傳送門2
兩道題大同小異,都是按要求建圖然後跑板子

第一道用矩陣樹定理跑比較慢,要把表打出來

第二道要高精,可以直接扔到 \(OEIS\) 得到遞推式

黑暗前的幻想鄉

題目傳送門

矩陣樹定理+容斥

最小生成樹計數

題目傳送門

最小生成樹有一個性質:同一個圖的每個最小生成樹中,邊權相等的邊數量相等。

所以我們可以先跑一遍最小生成樹,把必須選的邊權求出來

然後對於每一種必須選的邊權,先將最小生成樹中邊權為其它值的邊形成的聯通塊搞到一起

再把所有邊權為當前值的邊加進新圖裡跑矩陣樹定理

根據乘法原理,最終的答案即為所有方案的乘積