定义
在图论中,矩阵树定理((matrix tree theorem))是指,图的生成树数量等于调和矩阵的行列式(所以需要时间多项式计算)。
前置知识:行列式
定义
对于一个矩阵 (A[1...n][1...n]) ,其行列式为
(det(A)=sumlimits_{P}(-1)^{mu(P)}prodlimits_{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
",ans%mod);
return 0;
}
题目
小 Z 的房间
题目传送门
对于矩阵中的每一个点,都向右和向下扩展
如果可以连边就连边,跑一遍板子就可以了
要注意的是没有边的节点不要计算,否则会使乘积为 (0)
重建
题目传送门
要能清楚题目要求的是什么,矩阵树定理求出来的是什么
按照新的边权跑一遍矩阵树定理即可
要注意不能让分母为零,可以给它赋一个较小的数
生成树 && 轮状病毒
题目传送门1
题目传送门2
两道题大同小异,都是按要求建图然后跑板子
第一道用矩阵树定理跑比较慢,要把表打出来
第二道要高精,可以直接扔到 (OEIS) 得到递推式
黑暗前的幻想乡
矩阵树定理+容斥
最小生成树计数
最小生成树有一个性质:同一个图的每个最小生成树中,边权相等的边数量相等。
所以我们可以先跑一遍最小生成树,把必须选的边权求出来
然后对于每一种必须选的边权,先将最小生成树中边权为其它值的边形成的联通块搞到一起
再把所有边权为当前值的边加进新图里跑矩阵树定理
根据乘法原理,最终的答案即为所有方案的乘积