生成树计数 Matrix-Tree 定理 学习笔记
一直都知道要用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=+;
int n,m; LL K[N][N];
LL det(int n){ //求矩阵K的n-1阶顺序主子式
LL ret=;
for(int i=;i<=n-;i++){ //枚举主对角线上第i个元素
for(int j=i+;j<=n-;j++){ //枚举剩下的行
while(K[j][i]){ //辗转相除
int t=K[i][i]/K[j][i];
for(int k=i;k<=n-;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,,sizeof(K));
for (int i=;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\n",det(n));
}
return ;
}
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 = ;
const int N = +;
const int dx[] = {,,-,,-,-,,};
const int dy[] = {-,,,,-,,-,};
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=;
for(int i=; i<=n; i++) {
for(int k=i+; 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+,edge+m+);
for(int i=; i<=n; i++)
father[i]=i;
int cnt=;
for(int i=; 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=; 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-; 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,,sizeof(G));
for(int i=; 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=; i<=m; i++)
scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].dis); int cnt=Kruskal();
if(cnt!=n-) {
printf("0\n");
}
else{
int res=;
for(int i=; i<=tot; i++) {
for(int i=; i<=n; i++)
father[i]=i; addTreeEdge(val[i]); int blo=;
for(int i=; i<=n; i++)
if(Find(i)==i)
bel[i]=++blo;
for(int i=; i<=n; i++)
bel[i]=bel[Find(i)]; rebuild(val[i]);
res=1LL*res*Gauss(blo-)%MOD;
}
printf("%d\n",res);
}
return ;
}
有向图的生成树计数有两种:①给出有向图和其中的一个点,求以这个点为根的生成外向树个数。②给出有向图和其中一个点,求以这个点为根的生成内向树个数。
这里贴一下上面提到的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行处。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> pii;
const int N=;
const int P=1e9+;
int n,m;
vector<pii> G[N]; LL K[N][N];
LL det(int n){ //求矩阵K的n-1阶顺序主子式
LL ret=;
for(int i=;i<=n-;i++){ //枚举主对角线上第i个元素
for(int j=i+;j<=n-;j++){ //枚举剩下的行
while(K[j][i]){ //辗转相除
LL t=K[i][i]/K[j][i];
for(int k=i;k<=n-;k++) //转为倒三角
K[i][k]=K[i][k]-t*K[j][k],K[i][k]%=P;
swap(K[i],K[j]); //交换i、j两行
ret=-ret; //取负
}
}
ret=ret*K[i][i]%P;
}
return (ret%P+P)%P;
} LL solve(int s) {
for (int i=;i<=n;i++) for (int j=;j<=n;j++) K[i][j]=;
int num=;
for (int i=;i<n;i++) {
if ((<<(i-))&s) {
num++;
for (int j=;j<G[i].size();j++) {
int x=G[i][j].first,y=G[i][j].second;
K[x][x]++; K[y][y]++;
K[x][y]--; K[y][x]--;
}
}
}
if ((n--num)&) return -*det(n); else return det(n);
} int main()
{
cin>>n;
for (int i=;i<n;i++) {
int t; scanf("%d",&t);
for (int j=;j<=t;j++) {
int x,y; scanf("%d%d",&x,&y);
G[i].push_back(make_pair(x,y));
}
}
LL ans=;
for (int i=;i<(<<(n-));i++) {
ans+=solve(i);
ans=(ans%P+P)%P;
}
cout<<ans<<endl;
return ;
}
洛谷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即可,这样不会损失太多的精度。
#include<bits/stdc++.h>
using namespace std;
#define double long double
typedef long long LL;
const int N=+;
const double eps=1e-;
int n,m; double K[N][N];
double gauss(int n) {
double ans=1.0;
for(int i=;i<n;i++) {
int mx=i;
for(int j=i+;j<n;j++) if(fabs(K[j][i])>fabs(K[mx][i])) mx=j;
if(mx!=i) for(int j=;j<n;j++) swap(K[i][j],K[mx][j]);
for(int k=i+;k<n;k++) {
double mul=K[k][i]/K[i][i];
for(int j=i;j<n;j++) K[k][j]-=K[i][j]*mul;
}
if(fabs(K[i][i])<eps) return ;
}
for(int i=;i<n;i++) ans*=K[i][i];
return ans;
} int main()
{
cin>>n;
double ans=;
for (int i=;i<=n;i++) for (int j=;j<=n;j++) {
if (i==j) { double t; scanf("%Lf",&t); continue; }
scanf("%Lf",&K[i][j]);
//if (K[i][j]<eps) K[i][j]=eps;
if (K[i][j]+eps>1.0) K[i][j]-=eps;
if (i>j) ans=ans*(-K[i][j]);
K[i][j]=K[i][j]/(-K[i][j]);
if (i>j) K[i][i]+=K[i][j],K[j][j]+=K[i][j];
K[i][j]*=-;
}
ans*=gauss(n);
printf("%.8Lf\n",ans);
return ;
}
生成树计数 Matrix-Tree 定理 学习笔记的更多相关文章
- @总结 - 7@ 生成树计数 —— matrix - tree 定理(矩阵树定理)与 prüfer 序列
目录 @0 - 参考资料@ @0.5 - 你所需要了解的线性代数知识@ @1 - 矩阵树定理主体@ @证明 part - 1@ @证明 part - 2@ @证明 part - 3@ @证明 part ...
- BZOJ.1016.[JSOI2008]最小生成树计数(Matrix Tree定理 Kruskal)
题目链接 最小生成树有两个性质: 1.在不同的MST中某种权值的边出现的次数是一定的. 2.在不同的MST中,连接完某种权值的边后,形成的连通块的状态是一样的. \(Solution1\) 由这两个性 ...
- [bzoj1016][JSOI2008]最小生成树计数 (Kruskal + Matrix Tree 定理)
Description 现在给出了一个简单无向加权图.你不满足于求出这个图的最小生成树,而希望知道这个图中有多少个不同的最小生成树.(如果两颗最小生成树中至少有一条边不同,则这两个最小生成树就是不同的 ...
- 【证明与推广与背诵】Matrix Tree定理和一些推广
[背诵手记]Matrix Tree定理和一些推广 结论 对于一个无向图\(G=(V,E)\),暂时钦定他是简单图,定义以下矩阵: (入)度数矩阵\(D\),其中\(D_{ii}=deg_i\).其他= ...
- 数学-Matrix Tree定理证明
老久没更了,冬令营也延期了(延期后岂不是志愿者得上学了?) 最近把之前欠了好久的债,诸如FFT和Matrix-Tree等的搞清楚了(啊我承认之前只会用,没有理解证明--),FFT老多人写,而Matri ...
- 2019国家集训队论文《整点计数》命题报告 学习笔记/Min25
\(2019\)国家集训队论文<整点计数>命题报告 学习笔记/\(Min25\) 补了个大坑 看了看提交记录,发现\(hz\)的\(xdm\)早过了... 前置知识,\(HAOI\)< ...
- BZOJ.4031.[HEOI2015]小Z的房间(Matrix Tree定理 辗转相除)
题目链接 辗转相除解行列式的具体实现? 行列式的基本性质. //864kb 64ms //裸的Matrix Tree定理.练习一下用辗转相除解行列式.(因为模数不是质数,所以不能直接乘逆元来高斯消元. ...
- 【机器学习】决策树(Decision Tree) 学习笔记
[机器学习]决策树(decision tree) 学习笔记 标签(空格分隔): 机器学习 决策树简介 决策树(decision tree)是一个树结构(可以是二叉树或非二叉树).其每个非叶节点表示一个 ...
- SPOJ.104.Highways([模板]Matrix Tree定理 生成树计数)
题目链接 \(Description\) 一个国家有1~n座城市,其中一些城市之间可以修建高速公路(无自环和重边). 求有多少种方案,选择修建一些高速公路,组成一个交通网络,使得任意两座城市之间恰好只 ...
随机推荐
- 运行 tensorboard
使用下面命令总是报错: tensorboard --logdir=mylogdir tensorboard --logdir='./mylogdir' 正确命令 tensorboard --logdi ...
- margin 和padding 的区别
margin是用来隔开元素与元素的间距:padding是用来隔开元素与内容的间隔.margin用于布局分开元素使元素与元素互不相干: padding用于元素与内容之间的间隔,让内容(文字)与(包裹)元 ...
- MyCat的启动
启动MyCat: ./mycat start 查看启动状态: ./mycat status 停止: ./mycat stop 重启: ./mycat restart
- 20180712-Java Character类
char ch = 'a';// Unicode for uppercase Greek omega characterchar uniChar = '\u039A';//字符数组char[] cha ...
- 20180911-Java实例01
Java 实例 – 如何编译 Java 文件 本文我们演示如何编译 HelloWorld.java 文件,其中 Java 代码如下: public class HelloWorld { public ...
- CocoaPods进阶:本地包管理
http://www.iwangke.me/2013/04/18/advanced-cocoapods/ 粉笔网的iOS工程师唐巧曾经写过一篇blog<使用CocoaPods来做iOS程序的包依 ...
- python 使用xlsxwriter的方法属性
http://xlsxwriter.readthedocs.io/format.html
- (\w+)\s*, \s*(\w+)
\s表示空格 \w表示任何字符,字母数字下划线 _就表示下划线
- AUC
https://www.cnblogs.com/earendil/p/9400275.html
- 第 4 章 前端基础之jquery
一.jQuery是什么? 1. jQuery由美国人John Resig创建,至今已吸引了来自世界各地的众多 javascript高手加入其team. 2. jQuery是继prototype之后又一 ...