矩阵树定理、BEST 定理

说句闲话。今天翻到一篇博客上来给放了个公式:

\[\sum_{i=0}^n\binom{2i}i\binom{2n-2i}{2i}=4^n \]

看起来就很不对劲。然后爆算了一波确实是错的。敬请注意。

然后不知道为什么要管 Vizing 定理叫维京定理。感觉不如味精定理。感觉不如胃镜定理。

是不是应该反思一下树上背包都能写挂的问题。或者反思一下是不是太过于信任大样例的问题。

感觉我再不打 rated 就要橙了。

矩阵树定理

oi-wiki 上边的矩阵树定理内容毫不客气地说真的跟个 jb 一样。并不是说讲的不对,只是我觉得大多数人都不想看这一大车定义和公式,而且还是线性代数的。

这玩意拿来计数一张图的生成树个数。直接上结论,证明不会。

以下默认没有自环。

原版

计数无向图生成树个数。

定义无向图的邻接矩阵 \(A\)\(A_{i,j}\)\(1\) 当且仅当 \(i,j\) 间有边。定义度数矩阵 \(D\) ,其中 \(D_{i,i}\) 为节点 \(i\) 度数。定义无向图的基尔霍夫矩阵为 \(K=D-A\)

那么随便找个 \(k\) 给矩阵 \(K\) 去掉第 \(k\) 行第 \(k\) 列得到 \(K'\),答案就是 \(\det(K')\)

一道例题:[HEOI2015]小 Z 的房间

建图套板子即可。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int mod=1000000000;
int n,m,cnt;
int a[610][610],id[15][15];
char s[15][15];
int gauss(int n){
    int ans=1,w=1;
	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			while(a[i][i]){
				int rate=a[j][i]/a[i][i];
				for(int k=i;k<=n;k++){
					a[j][k]=(a[j][k]-1ll*rate*a[i][k]%mod+mod)%mod;
				}
				for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
				w=-w;
			}
			for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
			w=-w;
		}
	}
	for(int i=1;i<=n;i++)ans=1ll*ans*a[i][i]%mod;
	ans=(1ll*ans*w%mod+mod)%mod;
	return ans;
}
void add(int x,int y){
    a[x][x]++;a[y][y]++;a[x][y]--;a[y][x]--;
}
signed main(){
	scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%s",s[i]+1);
        for(int j=1;j<=m;j++){
            if(s[i][j]=='.')id[i][j]=++cnt;
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(s[i][j]=='.'&&s[i+1][j]=='.')add(id[i][j],id[i+1][j]);
            if(s[i][j]=='.'&&s[i][j+1]=='.')add(id[i][j],id[i][j+1]);
        }
    }
    printf("%d\n",gauss(cnt-1));
	return 0;
}

边带权

对于边带权的情况,只需要把邻接矩阵的 \(A_{i,j}\) 改为 \(i,j\) 间所有边的边权和,度数矩阵的 \(D_{i,i}\) 改为从 \(i\) 连出去的所有边的边权和,得到的答案就是所有生成树边权乘积的和。

另一道例题:[SDOI2014]重建

一个生成树 \(T\) 的概率显然是

\[\prod_{e\in T}p_e\prod_{e\notin T}(1-p_e) \]

稍微转化一下:

\[\begin{aligned} &\prod_{e\in T}p_e\prod_{e\notin T}(1-p_e)\\ =&\prod_{e}(1-p_e)\prod_{e\in T}\frac{p_e}{1-p_e} \end{aligned} \]

那么把每条边的权值设为 \(\dfrac {p_e}{1-p_e}\) 就行了。如果是 \(1\) 的话减个 \(eps\) 就行。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const double eps=1e-8;
int n,m,cnt;
double a[60][60],p[60][60];
double gauss(int n){
    double ans=1;
    int w=1;
	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			while(fabs(a[i][i])>eps){
				double rate=a[j][i]/a[i][i];
				for(int k=i;k<=n;k++){
					a[j][k]=a[j][k]-rate*a[i][k];
				}
				for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
				w=-w;
			}
			for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
			w=-w;
		}
	}
	for(int i=1;i<=n;i++)ans=ans*a[i][i];
	ans=ans*w;
	return ans;
}
void add(int x,int y){
    a[x][x]++;a[y][y]++;a[x][y]--;a[y][x]--;
}
signed main(){
	scanf("%d",&n);
    double ret=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            scanf("%lf",&p[i][j]);
            if(i==j)continue;
            if(p[i][j]>1-eps)p[i][j]-=eps;
            if(i<j)ret*=1-p[i][j];
            p[i][j]/=1-p[i][j];
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            a[i][i]+=p[i][j];a[i][j]-=p[i][j];
        }
    }
    printf("%.4lf\n",gauss(n-1)*ret);
	return 0;
}

有向图

对于有向图,我们更改一下 \(A\)\(D\)。此处的 \(A\) 变成了有向图的邻接矩阵,而 \(D\) 有两种不同情况:

  1. \(D_{i,i}\) 为点 \(i\) 入度,此时求的是外向树(从根向外指)。
  2. \(D_{i,i}\) 为点 \(i\) 出度,此时求的是内向树(从外向根指)。

然后关于去掉一行一列的问题,有向图的问题中去掉哪一行那一列就是以哪个点为根。

当然也可以带权。以下为模板代码。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int mod=1000000007;
int n,m,cnt;
int a[610][610],id[15][15];
char s[15][15];
int gauss(int n){
    int ans=1,w=1;
	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			while(a[i][i]){
				int rate=a[j][i]/a[i][i];
				for(int k=i;k<=n;k++){
					a[j][k]=(a[j][k]-1ll*rate*a[i][k]%mod+mod)%mod;
				}
				for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
				w=-w;
			}
			for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
			w=-w;
		}
	}
	for(int i=1;i<=n;i++)ans=1ll*ans*a[i][i]%mod;
	ans=(1ll*ans*w%mod+mod)%mod;
	return ans;
}
void add(int x,int y,int w){
    (a[x][x]+=w)%=mod;(a[y][y]+=w)%=mod;a[x][y]=(a[x][y]-w+mod)%mod;a[y][x]=(a[y][x]-w+mod)%mod;
}
signed main(){
    int t;scanf("%d%d%d",&n,&m,&t);
    for(int i=1;i<=m;i++){
        int u,v,w;scanf("%d%d%d",&u,&v,&w);
        if(t==0)add(u,v,w);
        else{
            a[v-1][v-1]=(a[v-1][v-1]+w)%mod;;a[u-1][v-1]=(a[u-1][v-1]-w+mod)%mod;
        }
    }
    printf("%d\n",gauss(n-1));
	return 0;
}

BEST 定理

内容:有向图欧拉回路数目:

\[ans=T\prod_{i}(out_i-1)! \]

其中 \(T\) 是内向生成树个数(容易证明每个节点为根的答案都一样),\(out_i\)\(i\) 的出度。

注意这个没有算上从根开始的路径数目。所以如果从根开始走的路径不同的话需要乘上根节点的出度。

一道例题:「BZOJ3659」WHICH DREAMED IT

裸的 BEST 定理,注意答案乘上根节点出度。

然而这题特判非常之多。需要判是否是欧拉图、图是否联通、是否没有边等等情况。我甚至一度怀疑是不是我行列式求值噶了。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int mod=1000003;
int n,m,cnt;
int a[110][110],ind[110],out[110],jc[200010],fa[110];
int find(int x){
    return fa[x]==x?fa[x]:fa[x]=find(fa[x]);
}
int gauss(int n){
    int ans=1,w=1;
	for(int i=1;i<=n;i++){
        if(!out[i])continue;
		for(int j=i+1;j<=n;j++){
            if(!out[j])continue;
			while(a[i][i]){
				int rate=a[j][i]/a[i][i];
				for(int k=i;k<=n;k++){
					a[j][k]=(a[j][k]-1ll*rate*a[i][k]%mod+mod)%mod;
				}
				for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
				w=-w;
			}
			for(int k=1;k<=n;k++)swap(a[i][k],a[j][k]);
			w=-w;
		}
	}
	for(int i=1;i<=n;i++)if(out[i])ans=1ll*ans*a[i][i]%mod;
	ans=(1ll*ans*w%mod+mod)%mod;
	return ans;
}
void clear(){
    for(int i=1;i<=n;i++)ind[i]=out[i]=0,fa[i]=i;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++)a[i][j]=0;
    }
}
void solve(){
    scanf("%d",&n);clear();
    for(int i=1;i<=n;i++){
        scanf("%d",&m);
        for(int j=1;j<=m;j++){
            int x;scanf("%d",&x);
            ind[x]++;out[i]++;
            a[i][i]++;a[i][x]--;
            fa[find(x)]=find(i);
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            a[i][j]=(a[i][j]+mod)%mod;
        }
    }
    if(n==1){
        printf("%d\n",jc[out[1]]);return;
    }
    for(int i=1;i<=n;i++){
        if(ind[i]!=out[i]||(find(i)!=find(1)&&ind[i])){
            puts("0");return;
        }
    }
    bool jud=false;
    for(int i=1;i<=n;i++)jud=(out[i]);
    if(!jud){
        puts("1");return;
    }
    int ans=gauss(n-1);
    for(int i=1;i<=n;i++)if(out[i])ans=1ll*ans*jc[out[i]-1]%mod;
    ans=1ll*ans*out[1]%mod;
    printf("%d\n",ans);
}
int main(){
    jc[0]=1;
    for(int i=1;i<=200000;i++)jc[i]=1ll*jc[i-1]*i%mod;
    int tim;scanf("%d",&tim);
    while(tim--)solve();
    return 0;
}
posted @ 2023-02-11 20:14  gtm1514  阅读(37)  评论(0编辑  收藏  举报