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}\),新的行列式为\(b_{i,j}\),\(b_{i,j}\)是\(a_{i,j}\)通过交换\(m,n\)两行得来的。
可以知道对\(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;
}