一直都知道要用Matrix-Tree定理来解决生成树计数问题,但是拖到今天才来学。博主数学不好也只能跟着各位大佬博客学一下它的应用以及会做题,证明实在是不会。
推荐博客:
https://www.cnblogs.com/zj75211/p/8039443.html (Matrix-Tree定理)
https://blog.csdn.net/u011815404/article/details/99679527(无向图生成树/MST计数)
https://www.cnblogs.com/yangsongyi/p/10697176.html (全面点的生成树计数)
https://www.cnblogs.com/twilight-sx/p/9064208.html(总结)
那么比较常见的无向图生成树计数问题就三种:①生成树计数②同边权边较少的MST计数③同边权边较少的MST计数,针对这3个问题有3种解决办法。
最简单的生成树计数就是Matrix-Tree定理的模板题啦,直接求出Kirchhoff 矩阵然后求它的n-1阶行列式绝对值。
Highways
代码抄袭参考的大佬的。
#include<bits/stdc++.h> using namespace std; typedef long long LL; const int N=10+5; int n,m; LL K[N][N]; LL det(int n){ //求矩阵K的n-1阶顺序主子式 LL ret=1; for(int i=1;i<=n-1;i++){ //枚举主对角线上第i个元素 for(int j=i+1;j<=n-1;j++){ //枚举剩下的行 while(K[j][i]){ //辗转相除 int t=K[i][i]/K[j][i]; for(int k=i;k<=n-1;k++) //转为倒三角 K[i][k]=K[i][k]-t*K[j][k]; swap(K[i],K[j]); //交换i、j两行 ret=-ret; //取负 } } ret=ret*K[i][i]; } return abs(ret); } int main() { int T; cin>>T; while (T--) { scanf("%d%d",&n,&m); memset(K,0,sizeof(K)); for (int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); K[x][x]++; K[y][y]++; K[x][y]--; K[y][x]--; } printf("%lld ",det(n)); } return 0; }
MST计数的话,如果同边权边较少的话可以考虑用暴力。它基于MST的两条性质:
- 每种权值的边的数量是固定的
- 不同的生成树中,某一种权值的边任意加入需要的数量后,形成的联通块状态是相同的
意思就是对于所有的MST对于边权例如为w的边的数量是一定的。这就启发我们暴力搜索每种边权选的使用的边,然后用乘法原理计算出方案数。这种办法因为是枚举所有的使用同边权使用情况所以时间复杂度是O(2^k*m)(k是同边权边数)。但是因为这个解法有局限性所以也不是特别有用。
去掉同边权边数限制的话就是更广泛的最小生成树计数了,要用到缩点+Matrix-Tree定理的解法。具体就是还是枚举每个边权i,然后把非边权i的边加入到图中然后缩点,对缩完点之后的图求Kirchhoff 矩阵利用Matrix-Tree定理求出生成树个数那么这个就是边权i的方案数了,然后全部边权乘法原理乘起来就是答案了。
给出一幅图求MST数量,缩点+Matrix-Tree定理解决。(这道题的数据也可以用暴力搜索解决)
代码还没写这里先贴个大佬的模板:
#include<iostream> #include<cstdio> #include<cstdlib> #include<string> #include<cstring> #include<cmath> #include<ctime> #include<algorithm> #include<utility> #include<stack> #include<queue> #include<vector> #include<set> #include<map> #include<bitset> #define EPS 1e-9 #define PI acos(-1.0) #define INF 0x3f3f3f3f #define LL long long #define Pair pair<int,int> const int MOD = 31011; const int N = 1000+5; const int dx[] = {0,0,-1,1,-1,-1,1,1}; const int dy[] = {-1,1,0,0,-1,1,-1,1}; using namespace std; struct Edge { int x,y; int dis; bool operator < (const Edge &rhs) const { return dis<rhs.dis; } } edge[N],tr[N]; int n,m; int father[N]; int G[N][N]; int tot,bel[N],val[N]; int Find(int x) { if(father[x]!=x) return father[x]=Find(father[x]); return x; } int Gauss(int n) { int res=1; for(int i=1; i<=n; i++) { for(int k=i+1; k<=n; k++) { while(G[k][i]) { int d=G[i][i]/G[k][i]; for(int j=i; j<=n; j++) G[i][j]=(G[i][j]-1LL*d*G[k][j]%MOD+MOD)%MOD; swap(G[i],G[k]); res=-res; } } res=1LL*res*G[i][i]%MOD,res=(res+MOD)%MOD; } return res; } int Kruskal() { sort(edge+1,edge+m+1); for(int i=1; i<=n; i++) father[i]=i; int cnt=0; for(int i=1; i<=m; i++) { int fu=Find(edge[i].x); int fv=Find(edge[i].y); if(fu==fv) continue; father[fu]=fv,tr[++cnt]=edge[i]; if(edge[i].dis!=val[tot]) val[++tot]=edge[i].dis; } return cnt; } void addTreeEdge(int v) { for(int i=1; i<n&&tr[i].dis!=v; i++){ int x=tr[i].x; int y=tr[i].y; father[Find(x)]=Find(y); } for(int i=n-1; i&&tr[i].dis!=v; i--){ int x=tr[i].x; int y=tr[i].y; father[Find(x)]=Find(y); } } void rebuild(int v) { memset(G,0,sizeof(G)); for(int i=1; i<=m; i++){ if(edge[i].dis==v){ int x=bel[edge[i].x]; int y=bel[edge[i].y]; G[x][y]--; G[y][x]--; G[x][x]++; G[y][y]++; } } } int main() { scanf("%d%d",&n,&m); for(int i=1; i<=m; i++) scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].dis); int cnt=Kruskal(); if(cnt!=n-1) { printf("0 "); } else{ int res=1; for(int i=1; i<=tot; i++) { for(int i=1; i<=n; i++) father[i]=i; addTreeEdge(val[i]); int blo=0; for(int i=1; i<=n; i++) if(Find(i)==i) bel[i]=++blo; for(int i=1; i<=n; i++) bel[i]=bel[Find(i)]; rebuild(val[i]); res=1LL*res*Gauss(blo-1)%MOD; } printf("%d ",res); } return 0; }
有向图的生成树计数有两种:①给出有向图和其中的一个点,求以这个点为根的生成外向树个数。②给出有向图和其中一个点,求以这个点为根的生成内向树个数。
这里贴一下上面提到的Yang1208大佬的解决方法:
变元矩阵树定理:求所有生成树的总边积的和。
和矩阵树的求法相同,不过行列式中 a[i][i]a[i][i] 记录的是总的边权和,a[i][j]a[i][j] 记录 i,j之间边权的相反数。
这里有一个讲得很好的总结:https://www.cnblogs.com/reverymoon/p/9512836.html。
题目练习:
洛谷P4336 黑暗前的幻想乡
题意:有n个点n-1个公司(n<=17),每个公司能修建若干条边。问是否存在让每一个公司恰好修建一条边使得n个点形成一棵树,问方案数是多少。
解法:注意到n非常小,所以我们可以考虑2^n级别的算法。此题的 每个公司修建恰好一条 这个条件非常麻烦,所以我们考虑用容斥原理解决。我们设calc(i)calc(i)为假设钦定i个建筑公司不选,其他的随意的方案数,最后的答案即为:ans=calc(0)−calc(1)+calc(2)−……。(每一次容斥都用矩阵树定理求解)。
这里注意要取模,行列式的取模注意在18行和25行处。
1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long LL; 4 typedef pair<int,int> pii; 5 const int N=20; 6 const int P=1e9+7; 7 int n,m; 8 vector<pii> G[N]; 9 10 LL K[N][N]; 11 LL det(int n){ //求矩阵K的n-1阶顺序主子式 12 LL ret=1; 13 for(int i=1;i<=n-1;i++){ //枚举主对角线上第i个元素 14 for(int j=i+1;j<=n-1;j++){ //枚举剩下的行 15 while(K[j][i]){ //辗转相除 16 LL t=K[i][i]/K[j][i]; 17 for(int k=i;k<=n-1;k++) //转为倒三角 18 K[i][k]=K[i][k]-t*K[j][k],K[i][k]%=P; 19 swap(K[i],K[j]); //交换i、j两行 20 ret=-ret; //取负 21 } 22 } 23 ret=ret*K[i][i]%P; 24 } 25 return (ret%P+P)%P; 26 } 27 28 LL solve(int s) { 29 for (int i=1;i<=n;i++) for (int j=1;j<=n;j++) K[i][j]=0; 30 int num=0; 31 for (int i=1;i<n;i++) { 32 if ((1<<(i-1))&s) { 33 num++; 34 for (int j=0;j<G[i].size();j++) { 35 int x=G[i][j].first,y=G[i][j].second; 36 K[x][x]++; K[y][y]++; 37 K[x][y]--; K[y][x]--; 38 } 39 } 40 } 41 if ((n-1-num)&1) return -1*det(n); else return det(n); 42 } 43 44 int main() 45 { 46 cin>>n; 47 for (int i=1;i<n;i++) { 48 int t; scanf("%d",&t); 49 for (int j=1;j<=t;j++) { 50 int x,y; scanf("%d%d",&x,&y); 51 G[i].push_back(make_pair(x,y)); 52 } 53 } 54 LL ans=0; 55 for (int i=0;i<(1<<(n-1));i++) { 56 ans+=solve(i); 57 ans=(ans%P+P)%P; 58 } 59 cout<<ans<<endl; 60 return 0; 61 }
洛谷P3317 重建
题意:给出一个二维矩阵a,aij代表i点和j点的连通概率,问恰好有n-1条边连通使得n个点组成一棵树的概率。
解法:首先要学了变元矩阵树定理,其实也就是普通的矩阵树定理变了下矩阵的值然后求出来的东西也不一样了,本质求出来的是∑T is tree ∏(u,v)∈T Kuv 。
那么首先容易发现答案是,但是发现这个东西似乎没办法用矩阵树定理求,原因在于多了这一块东西很麻烦。那么我们想办法把提出了。得到得到这个东西就舒服了。
后面的我们可以令矩阵边长为 Awv/1-Awv然后用矩阵树定理求得,然后将结果再乘以前面的即可。
还有一些细节:因为我们改变了边长,所以如果Awv为1的时候会出现分母为0的情况。解决办法是当Awv->1.0,令Awv-=eps即可,这样不会损失太多的精度。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define double long double 4 typedef long long LL; 5 const int N=50+5; 6 const double eps=1e-8; 7 int n,m; 8 9 double K[N][N]; 10 double gauss(int n) { 11 double ans=1.0; 12 for(int i=1;i<n;i++) { 13 int mx=i; 14 for(int j=i+1;j<n;j++) if(fabs(K[j][i])>fabs(K[mx][i])) mx=j; 15 if(mx!=i) for(int j=1;j<n;j++) swap(K[i][j],K[mx][j]); 16 for(int k=i+1;k<n;k++) { 17 double mul=K[k][i]/K[i][i]; 18 for(int j=i;j<n;j++) K[k][j]-=K[i][j]*mul; 19 } 20 if(fabs(K[i][i])<eps) return 0; 21 } 22 for(int i=1;i<n;i++) ans*=K[i][i]; 23 return ans; 24 } 25 26 int main() 27 { 28 cin>>n; 29 double ans=1; 30 for (int i=1;i<=n;i++) for (int j=1;j<=n;j++) { 31 if (i==j) { double t; scanf("%Lf",&t); continue; } 32 scanf("%Lf",&K[i][j]); 33 //if (K[i][j]<eps) K[i][j]=eps; 34 if (K[i][j]+eps>1.0) K[i][j]-=eps; 35 if (i>j) ans=ans*(1-K[i][j]); 36 K[i][j]=K[i][j]/(1-K[i][j]); 37 if (i>j) K[i][i]+=K[i][j],K[j][j]+=K[i][j]; 38 K[i][j]*=-1; 39 } 40 ans*=gauss(n); 41 printf("%.8Lf ",ans); 42 return 0; 43 }