【模版】生成树计数

无向图生成树计数(Matrix-Tree)

对于生成树的计数,一般采用矩阵树定理(Matrix-Tree 定理)来解决。

Matrix-Tree 定理的内容为:对于已经得出的基尔霍夫矩阵,去掉其随意一行一列得出的矩阵的行列式,其绝对值为生成树的个数

因此,对于给定的图 G,若要求其生成树个数,可以先求其基尔霍夫矩阵,然后随意取其任意一个 n-1 阶行列式,然后求出行列式的值,其绝对值就是这个图中生成树的个数

  • 度数矩阵 D[G]当$ i≠j$ 时,(D[i][j]=0),当$ i=j$ 时,(D[i][i] = degree(v_i))

  • 邻接矩阵 A[G](v_i)(v_j) 有边连接时,(A[i][j]=1),当 (v_i)(v_j) 无边连接时,(A[i][j]=0)

  • 基尔霍夫矩阵(Kirchhoff) K[G]:也称拉普拉斯算子,其定义为(K[G]=D[G]-A[G]),即:(K[i][j]=D[i][j]-A[i][j])

取模

  • 图中节点的下标从0开始计数!
  • 不存在自环,允许存在重边
  • 求行列式参数为n,求生成树计数参数为n-1
typedef long long ll;
ll mod;

const int N = 205;

struct Matrix {
    ll mat[N][N];
    void init() {
        memset(mat,0,sizeof(mat));
    }
    void addEdge(int u,int v) {
        mat[u][v]--;
        mat[u][u]++;
    }
    ll det(int n){
        ll res=1;
        for(int i=0;i<n;++i){
            if(!mat[i][i]){
                bool flag=false;
                for(int j=i+1;j<n;++j){
                    if(mat[j][i]){
                        flag=true;
                        for(int k=i;k<n;++k) swap(mat[i][k],mat[j][k]);
                        res=-res;
                        break;
                    }
                }
                if(!flag) return 0;
            }
            for(int j=i+1;j<n;++j){
                while(mat[j][i]){
                    ll t=mat[i][i]/mat[j][i];
                    for(int k=i;k<n;++k){
                        mat[i][k]=(mat[i][k]-t*mat[j][k])%mod;
                        swap(mat[i][k],mat[j][k]);
                    }
                    res=-res;
                }
            }
            res*=mat[i][i];
            res%=mod;//模意义下的语句,不是模意义则不加
        }
        if(res<0) res+=mod;
        return res;
    }
}ret;

调用(求行列式):

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        scanf("%lld", &ret.mat[i][j]);
    }
}
printf("%lld
", ret.det(n));

取逆元(mod为质数)

  • 图中节点的下标从0开始计数!
  • 不存在自环,允许存在重边
  • 求行列式参数为n,求生成树计数参数为n-1
typedef long long ll;
const ll mod = 998244353;

const int N = 105;
const int M = 1e4 + 5;

ll inv(ll a) {
    if(a == 1)return 1;
    return inv(mod%a)*(mod-mod/a)%mod;
}

struct Matrix {
    ll mat[N][N];
    void init() {
        memset(mat,0,sizeof(mat));
    }
    void addEdge(int u,int v) {
        mat[u][v]--;
        mat[u][u]++;
    }
    ll det(int n) { //求行列式的值模上MOD,需要使用逆元
        for(int i = 0; i < n; i++)
            for(int j = 0; j < n; j++)
                mat[i][j] = (mat[i][j]%mod+mod)%mod;
        ll res = 1;
        for(int i = 0; i < n; i++) {
            for(int j = i; j < n; j++)
                if(mat[j][i]!=0) {
                    for(int k = i; k < n; k++)
                        swap(mat[i][k],mat[j][k]);
                    if(i != j)
                        res = (-res+mod)%mod;
                    break;
                }
            if(mat[i][i] == 0) {
                res = 0;//不存在(也就是行列式值为0)
                break;
            }
            for(int j = i+1; j < n; j++) {
                //int mut = (mat[j][i]*INV[mat[i][i]])%MOD;//打表逆元
                ll mut = (mat[j][i]*inv(mat[i][i]))%mod;
                for(int k = i; k < n; k++)
                    mat[j][k] = (mat[j][k]-(mat[i][k]*mut)%mod+mod)%mod;
            }
            res = (res * mat[i][i])%mod;
        }
        return res;
    }
}ret;

调用:

ret.init();
for (int i = 1; i <= m; i++) {
    scanf("%d%d%lld", &e[i].u, &e[i].v, &e[i].w);
    ret.addEdge(--e[i].u, --e[i].v);
    ret.addEdge(e[i].v, e[i].u);
}
ll tot = ret.det(n-1);
if(tot == -1) {
    puts("0");
    continue;
}

不取模

  • 图中节点的下标从1开始计数!
  • 不存在自环,允许存在重边
  • 求行列式参数为n,求生成树计数参数为n-1
typedef long long ll;

const int N = 55;

struct Matrix {
    ll mat[N][N];
    void init() {
        memset(mat, 0, sizeof mat);
    }
    ll gauss(int n) {
        ll res = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = i + 1; j <= n; j++) {
                while (mat[j][i]) {
                    ll t = mat[i][i] / mat[j][i];
                    for (int k = i; k <= n; k++)
                        mat[i][k] = (mat[i][k] - t * mat[j][k]);
                    swap(mat[i], mat[j]);
                    res = -res;
                }
            }
            if(mat[i][i] == 0) return 0;
            res = res * mat[i][i];
        }
        if(res < 0) res = -res;
        return res;
    }
    void add(int u, int v) {
        mat[u][u]++;
        mat[v][v]++;
        mat[u][v]--;
        mat[v][u]--;
    }
}ret;

调用:

ret.init();
for (int i = 1; i <= m; i++) {
    int u, v;
    scanf("%d%d", &u, &v);
    ret.add(u, v);
}
printf("%lld
", ret.gauss(n-1));

最小生成树计数(Kruskal+Matrix-Tree)

用kruscal计算最小生成树时,每次取连接了两个不同联通块的最小的边。也就是先处理(d_1)(c_1)长度的边,再处理(d_2)(c_2)长度的边。长度相同的边无论怎么选,最大联通情况都是固定的。 分别对(c_i)长度的边产生的几个联通块计算生成树数量再乘起来,然后把这些联通块缩点,再计算(c_{i+1})长度的边。

并查集(fa[i])是当前长度之前,节点所属的联通块,(ka[i])是当前长度的边连接后它在的联通块。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const int N=101;
const int M=1001;
ll n,m,p,ans;
vector<int>gra[N];
struct edge{
    int u,v,w;
}e[M];
int cmp(edge a,edge b){
    return a.w<b.w;
}
ll mat[N][N],g[N][N];
ll fa[N],ka[N],vis[N];
ll det(ll c[][N],ll n){
    ll i,j,k,t,ret=1;
    for(i=0;i<n;i++)
    for(j=0;j<n;j++) c[i][j]%=p;
    for(i=0; i<n; i++){
        for(j=i+1; j<n; j++)
            while(c[j][i]){
                t=c[i][i]/c[j][i];
                for(k=i; k<n; k++)
                    c[i][k]=(c[i][k]-c[j][k]*t)%p;
                swap(c[i],c[j]);
                ret=-ret;
            }
        if(c[i][i]==0)
            return 0L;
        ret=ret*c[i][i]%p;
    }
    return (ret+p)%p;
}
ll find(ll a,ll f[]){
    return f[a]==a?a:find(f[a],f);
}
void matrix_tree(){//对当前长度的边连接的每个联通块计算生成树个数
    for(int i=0;i<n;i++)if(vis[i]){//当前长度的边连接了i节点
        gra[find(i,ka)].push_back(i);//将i节点压入所属的联通块
        vis[i]=0;//一边清空vis数组
    }
    for(int i=0;i<n;i++)
    if(gra[i].size()>1){//联通块的点数为1时生成树数量是1
        memset(mat,0,sizeof mat);//清空矩阵
        int len=gra[i].size();
        for(int j=0;j<len;j++)
        for(int k=j+1;k<len;k++){//构造这个联通块的矩阵(有重边)
            int u=gra[i][j],v=gra[i][k];
            if(g[u][v]){
                mat[k][j]=(mat[j][k]-=g[u][v]);
                mat[k][k]+=g[u][v];mat[j][j]+=g[u][v];
            }
        }
        ans=ans*det(mat,gra[i].size()-1)%p;
        for(int j=0;j<len;j++)fa[gra[i][j]]=i;//缩点
    }
    for(int i=0;i<n;i++)
    {
        gra[i].clear();
        ka[i]=fa[i]=find(i,fa);
    }
}
int main(){
    while(scanf("%lld%lld%lld",&n,&m,&p),n){
        for(int i=0;i<m;i++){
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            u--;v--;
            e[i]=(edge){u,v,w};
        }
        sort(e,e+m,cmp);
        memset(g,0,sizeof g);
        ans=1;
        for(ll i=0;i<n;i++)ka[i]=fa[i]=i;
        for(ll i=0;i<=m;i++){//边从小到大加入
            if(i&&e[i].w!=e[i-1].w||i==m)//处理完长度为e[i-1].w的所有边
                matrix_tree();//计算生成树
            ll u=find(e[i].u,fa),v=find(e[i].v,fa);//连的两个缩点后的点
            if(u!=v)//如果不是一个
            {
                vis[v]=vis[u]=1;
                ka[find(u,ka)]=find(v,ka);//两个分量在一个联通块里。
                g[u][v]++,g[v][u]++;//邻接矩阵
            }
        }
        int flag=1;
        for(int i=1;i<n;i++)if(fa[i]!=fa[i-1])flag=0;
        printf("%lld
",flag?ans%p:0);//注意p可能为1,这样m=0时如果ans不%p就会输出1
    }
}
原文地址:https://www.cnblogs.com/Waldeinsamkeit/p/13450707.html