【暖*墟】#图论计数# 矩阵树定理的学习与练习

【基本概念及实现】

 

矩阵树定理用于求解图上生成树的个数。

 

实现方式是:A为邻接矩阵,D为度数矩阵,则基尔霍夫(Kirchhoff)矩阵即为:K=D−A。

 

得到K:记a为Kirchhoff矩阵,若存在E(u,v),则a[u][u]++,a[v][v]++,a[u][v]−−,a[v][u]−−。

那么,a[i][i]为i点的度数,a[i][j]为 i,j之间边的条数的相反数,就构造出K矩阵了。

 

  • 这样构成的矩阵K的行列式的值,就为此图中生成树的个数。

 

求解行列式的快速方法:使用高斯消元进行消元消出上三角矩阵,则有'对角线上的值的乘积=行列式的值'。

 

一般而言求解生成树个数的题目数量会非常庞大,需要取模处理。

有些情况下模数不为质数,因此不能直接在模意义下消元,需要使用辗转相除法。

 

int Gauss(){

  int ans=1; for(int i=1;i<=tot;i++){

    for(int j=i+1;j<=tot;j++) //tot是总点数

          while(f[j][i]){ int t=f[i][i]/f[j][i];

        for(int k=i;k<=tot;k++)

            f[i][k]=(f[i][k]-t*f[j][k]%mod+mod)%mod,

swap(f[i][k],f[j][k]); ans
=-ans; //辗转相除法 } ans=(ans*f[i][i])%mod; } return (ans+mod)%mod; //注意ans可能为负数 }

 

变元矩阵树定理:求所有生成树的总边积的和。

  • 和矩阵树的求法类似,a[i][i]记录总的边权和,a[i][j]记录i,j之间边权的相反数。

 


 

【例题1】P4111 小Z的房间

 

  • 你突然有了一个大房子,房子里面有一些房间。房子可以看做是一个包含n×m个格子的矩形,
  • 每个格子是一个房间或者是一个柱子。在一开始的时候,相邻的格子之间都有墙隔着。
  • 你想要打通一些相邻房间的墙,使得所有房间能够互相到达。
  • 在此过程中,你不能把房子给打穿,或者打通柱子(以及柱子旁边的墙)。
  • 同时,你不希望在房子中有小偷的时候会很难抓,所以你希望任意两个房间之间都只有一条通路。
  • 现在,你希望统计一共有多少种可行的方案。答案对10^9 取模。1<=n,m<=9。

 

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<string>
#include<cmath>
using namespace std;
typedef long long ll;

//【p4111】小Z的房间 // 求图中生成树(所有节点全部连通的树)的数量

//【矩阵树定理】Kirchhoff矩阵的行列式的值,就是图中生成树的个数。

// 注意:不能加入柱子的点。加边时,只枚举(i,j)上方和左方的点,可以避免重复。

void reads(int &x){ //读入优化(正负整数)
    int fx_=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx_=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fx_; //正负号
}

const int mod=1e9; int n,m,f[109][109],id[19][19],tot=0;

void add(int u,int v){ f[u][v]--,f[v][u]--,f[u][u]++,f[v][v]++; }

int Gauss(){
  int ans=1; for(int i=1;i<tot;i++){
    for(int j=i+1;j<tot;j++) //tot是总点数
      while(f[j][i]){ int t=f[i][i]/f[j][i];
        for(int k=i;k<tot;k++) 
            f[i][k]=(f[i][k]-1LL*t*f[j][k]%mod+mod)%mod;
        std::swap(f[i],f[j]),ans=-ans; //辗转相除法
      } ans=(1LL*ans*f[i][i])%mod;
  } return (ans+mod)%mod; //注意ans可能为负数
}

char ss[19][19];

int main(){
    reads(n),reads(m); for(int i=1;i<=n;i++) scanf("%s",ss[i]+1);
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) 
        if(ss[i][j]=='.') id[i][j]=++tot; //给每个非墙的点(构成树的点)编号
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(ss[i][j]=='.'){
        if(id[i-1][j]) add(id[i][j],id[i-1][j]); //向上方连边
        if(id[i][j-1]) add(id[i][j],id[i][j-1]); //向左边连边
    } printf("%d\n",Gauss()); //矩阵树定理求图中生成树的数量
}
【p4111】小Z的房间 // 求图中生成树(所有节点全部连通的树)的数量

 

细节问题就是:

for(int k=i;k<tot;k++)

    f[i][k]=(f[i][k]-1LL*t*f[j][k]%mod+mod)%mod;

std::swap(f[i],f[j]),ans=-ans; //辗转相除法

 

在本地编译不过,所以也可以写成:

for(int k=i;k<tot;k++)

    f[i][k]=(f[i][k]-1LL*t*f[j][k]%mod+mod)%mod,

    swap(f[i][k],f[j][k]); ans=-ans; //辗转相除法

 


 

【例题2】P4336 黑暗前的幻想乡

 

  • n个点、n−1种颜色,某种颜色可以涂某些边。
  • 求图中n-1条边都不同色的生成树的数量 % 10^9+7 。

 

【容斥原理】合法方案数=总生成树方案数-缺一种颜色的方案数+缺两种颜色的方案数……

 

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<string>
#include<cmath>
using namespace std;
typedef long long ll;

//【p4336】黑暗前的幻想乡 // 矩阵树定理 + 容斥原理

//n个点、n−1种颜色,某种颜色可以涂某些边。
//求图中n-1条边都不同色的生成树的数量 % 10^9+7 。

//【矩阵树定理】Kirchhoff矩阵的行列式的值,就是图中生成树的个数。

//【容斥原理】合法方案数=总生成树方案数-缺一种颜色的方案数+缺两种颜色的方案数……

void reads(int &x){ //读入优化(正负整数)
    int fx_=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx_=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fx_; //正负号
}

const int mod=1e9+7; int n,m,f[19][19],anss=0;

void add(int u,int v){ f[u][v]--,f[v][u]--,f[u][u]++,f[v][v]++; }

int Gauss(){
  int ans=1; for(int i=1;i<n;i++){
    for(int j=i+1;j<n;j++) //n是总点数
      while(f[j][i]){ int t=f[i][i]/f[j][i];
        for(int k=i;k<n;k++) 
          f[i][k]=(f[i][k]-1LL*t*f[j][k]%mod+mod)%mod,
          swap(f[i][k],f[j][k]); ans=-ans; //辗转相除法
      } ans=(1LL*ans*f[i][i])%mod;
  } return (ans+mod)%mod; //注意ans可能为负数
}

vector < pair<int,int> > q[19]; 

int main(){
    reads(n); for(int i=1;i<n;i++)
      { reads(m); for(int j=1,x,y;j<=m;j++)
          reads(x),reads(y),q[i].push_back(make_pair(x,y)); }
    for(int i=0;i<(1<<(n-1));i++){ //枚举某种修建状态【穷举+容斥】
        int cnt=0; memset(f,0,sizeof(f));
        for(int j=1;j<n;j++) if(i&(1<<(j-1))){
            for(int k=0;k<q[j].size();k++) //如果状态i中修了边j
                add(q[j][k].first,q[j][k].second); //将边放入矩阵K
            cnt++; //此状态i下,修建的边数
        } if((n-cnt)&1) anss=(anss+Gauss())%mod;
          else anss=(anss-Gauss()+mod)%mod;
    } cout<<anss<<endl; //最终得到的答案:
}
【p4336】黑暗前的幻想乡 // 矩阵树定理 + 容斥原理

 

 

 

                                               ——时间划过风的轨迹,那个少年,还在等你。

 

posted @ 2019-03-13 17:21  花神&缘浅flora  阅读(274)  评论(0编辑  收藏  举报