2018.09.22【JSOI2008】【BZOJ1016】最小生成树计数(矩阵树定理)(并查集)

版权声明:转载请声明出处,谢谢配合。 https://blog.csdn.net/zxyoi_dreamer/article/details/82813258

传送门


解析:

好的这是一道需要数学推理的矩阵树题目。

是的,我们还会用到并查集,就像 K r u s k a l Kruskal 一样。

首先我们考虑一个问题。

前置定理

我们先随便做一棵最小生成树。
重要定理:那么在这棵生成树中如果权值为 w w 的边有 t t 条,那么在所有最小生成树中,权值为 w w 的边都有 k k 条。
证明如下:
考虑在这棵生成树中断掉一条权值为 w w 的边,使其分为两个联通分量。再次连一条新边,生成一个与原树不同的最小生成树。
如果这两个联通分量中间只有这一条边,那就没有考虑的必要了,它作为桥必然出现在生成树上。

如果有其他边?

我们随便选取一条边连接着两个联通分量。

令新边权值为 w w' 考虑如下几种情况:
1. w < w 1.w'<w ,显然,新的生成树比原来的小,与原树是最小生成树矛盾。
2. w < w 2.w'<w ,显然,新的生成树比原来的大,那么新树就不是最小生成树。
所以,要再次生成最小生成树,只能令 w = = w w'==w 。即权值为 w w 的边数量仍然为 t t ,归纳一下,原命题得证。

解题思路:

我们把边权相同的边一起考虑。

显然,这些边会使得一些顶点连接在一起,形成几个联通分量。
然后我们将这些联通分量缩点。我们用下一个权值继续在缩点后的图上做连接。继续缩点。

根据乘法原理,每个联通分量(以边权相同,分阶段考虑)的生成树个数乘积就是总生成树个数。

缩点用并查集实现。


代码(巨大常数警告,其实也不是很大 ):

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define re register
#define gc getchar
#define pc putchar
#define cs const
#define int ll 

cs int mod=31011;

inline
int getint(){
	re int num=0;
	re char c;
	while(!isdigit(c=gc()));
	while(isdigit(c))num=(num<<1)+(num<<3)+(c^48),c=gc();
	return num;
}

struct matrix{
	int arr[101][101];
	void reset(){memset(arr,0,sizeof arr);}
	int det(int n){
		int res=1;
		for(int re i=1;i<=n;++i){
			for(int re j=i+1;j<=n;++j){
				while(arr[j][i]){
					int tmp=arr[i][i]/arr[j][i];
					for(int re k=i;k<=n;++k)arr[i][k]=(arr[i][k]-arr[j][k]*tmp)%mod;
					for(int re k=i;k<=n;++k)swap(arr[i][k],arr[j][k]);
					res=-res; 
				}
			}
			res=(res*arr[i][i])%mod;
		}
		return res;
	}
	
	int matrix_tree(int n){
		return det(n-1);
	}
	
	int *const operator[](cs int &offset){
		return arr[offset];
	}
	
}C;

int n,tot,cnt,m,ans=1;
bool mark[101];
int q[101],pos[101];
int D[101][101];
bool vis[101];
int fa[101];
inline
int getfa(int x){
	return x==fa[x]?x:fa[x]=getfa(fa[x]);
}

inline
void merge(int i,int j){
	i=getfa(i),j=getfa(j);
	fa[i]=j;
}

inline
void init(){
	for(int re i=1;i<=n;++i)fa[i]=i;
}

struct edge{
	int u,v,w;
	friend bool operator<(cs edge &a,cs edge &b){
		return a.w<b.w;
	}
}e[10002];

signed main(){
	n=getint();
	init();
	m=getint();
	for(int re i=1;i<=m;++i){
		e[i].u=getint();
		e[i].v=getint();
		e[i].w=getint();
		merge(e[i].u,e[i].v);
	}
	{//这种写法能够令C成为局部变量,在一些卡空间的题目里面有奇效,这里只是个人习惯。
		int c=0;
		for(int re i=1;i<=n;++i)if(fa[i]==i)++c;
		if(c>1){
			puts("0");
			return 0;
		}
	}
	init();
	sort(e+1,e+m+1);
	for(int re i=1,j=1;i<=m;i=++j){
		while(e[j+1].w==e[i].w&&j+1<=m)++j;
		memset(D,0,sizeof D);
		tot=cnt=0;
		memset(mark,0,sizeof mark);
		memset(vis,0,sizeof vis);
		C.reset();
		for(int re k=i;k<=j;++k){
			int u=getfa(e[k].u);
			int v=getfa(e[k].v);
			if(u!=v){
				if(!mark[u])mark[u]=true,q[++tot]=u;
				if(!mark[v])mark[v]=true,q[++tot]=v;
				++D[u][u],++D[v][v];
				--D[v][u],--D[u][v];
			}
		}
		for(int re k=i;k<=j;++k)merge(e[k].u,e[k].v);
		for(int re k=1;k<=tot;++k){
			if(!vis[k]){
				vis[k]=true,pos[cnt=1]=q[k];
				for(int re l=k+1;l<=tot;++l){
					if(getfa(q[k])==getfa(q[l])){
						pos[++cnt]=q[l];
						vis[l]=true;
					}
				}
				for(int re p=1;p<=cnt;++p){
					for(int re s=1;s<=cnt;++s){
						C[p][s]=D[pos[p]][pos[s]];
					}
				}
				ans=(ans*C.matrix_tree(cnt))%mod;
			}
		}
	}
	cout<<(ans+mod)%mod;
	return 0;
}

猜你喜欢

转载自blog.csdn.net/zxyoi_dreamer/article/details/82813258
今日推荐