Prufer编码:51nod 1806 wangyurzee的树

题目描述:戳这里
题解:
这题用到了一个叫做prufer编码的东西,感觉还是比较有用的。

首先题目给了若干限制条件,某一个点的度数不能为某一个(或者一些)值。这个东西显然比较难求,那么我们可以容斥一下。我们把条件变成有多少点的度数为某一个值,那么可以求出有至少多少条件被满足了,答案就是恰好有0个条件被满足的方案数(观察到条件总数比较小,可以暴枚)。

那么题目就变成求限定某些点的度数的生成树(无根树)的数量。
简化一下问题,我们先求n个点的生成树的数量

这里就要引入prufer编码的概念了。
prufer编码是一种生成方式,可以把一颗无根树(点不相同)变成一个长度为n-2的序列,也可以把一个prufer序列变成一颗无根树。
那么接下来就讲讲具体生成方法(这里只讨论点数大于1的树)。
1.树->序列
我们每次找到一个编号最小的叶子节点,把它从树中取出,并且把它的父节点的编号放入序列中,直到树中只剩下两个点,此时停止。
2.序列->树
设集合 A − > 1 , 2 , 3 , . . . , n − 1 , n A -> {1,2,3,...,n-1,n} A>1,2,3,...,n1,n
purfer序列 a 1 , a 2 , . . . , a n − 2 a_1,a_2,... ,a_{n-2} a1,a2,...,an2
顺次选出purfer数列首位元素,然后在集合A中选出另一元素与它相连边并且去掉
选出元素需满足三个条件
1.不能在prufer序列中
2.应在集合A中
3.序号最小
不断进行以上操作,直到prufer数列为空。此时A集合必然存在两个元素,将这两个元素连接起来。
那么由于以上方法使得一颗无根树和一个序列一一对应,所以可以证明这样的生成方式是唯一的。

知道了这个条件对我们有什么帮助呢。
我们可以先解决简单的问题:n个点的生成树的数量
这个东西直接就可以写出来了: n n − 2 n^{n-2} nn2(prufer序列中的每一个元素都从1~n)

那么如果一些点的度数有限制呢?

那么考虑度数和边数有关,一个点的度数为x,那么它肯定在prufer序列中出现了x-1次。
这个性质显然,因为出现了x-1次以后,这个点就变成了叶节点。

那么如果一些点的度数确定了,生成树的方案数就可以求了。

假设总共有n个点,有x个点是没有限制的,y个点有限制,度数分别为 d u 1 , d u 2 , . . . , d u y {du_1,du_2,...,du_y} du1,du2,...,duy ∑ d u i − 1 = D \sum du_i-1=D dui1=D
那么x个点可以随便放,那么就是 x n − 2 − D x^{n-2-D} xn2D
剩下的点分别在purfer序列中出现了 d u 1 − 1 , d u 2 − 1 , . . . , d u y − 1 {du_1-1,du_2-1,...,du_y-1} du11,du21,...,duy1次,就是重排列的方案数:
( n − 2 − D ) ! ( d u 1 − 1 ) ! ( d u 2 − 1 ) ! . . . ( d u y − 1 ) ! \frac{(n-2-D)!}{(du_1-1)!(du_2-1)!...(du_y-1)!} (du11)!(du21)!...(duy1)!(n2D)!
所以总共的方案数就是 ( n − 2 − D ) ! ( d u 1 − 1 ) ! ( d u 2 − 1 ) ! . . . ( d u y − 1 ) ! x n − 2 − D \frac{(n-2-D)!}{(du_1-1)!(du_2-1)!...(du_y-1)!}x^{n-2-D} (du11)!(du21)!...(duy1)!(n2D)!xn2D

公式套一套,就可一做出来了。

还有几个小坑:
1.树的大小如果为1,直接输出1(prufer序列不能处理!!!)
2.一个点可能有多个限制条件,如果同时满足了多个条件,那么直接continue

代码如下:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1000005,maxm=20,tt=1e9+7;
int n,m,u[maxm],d[maxm],pw[maxn],vis[maxn];
ll ans;
ll qsm(ll x,int y){
	ll ret=1;
	while (y){
		if (y%2==1) ret=1ll*ret*x%tt;
		x=x*x%tt; y/=2;
	}
	return ret;
}
int main(){
	scanf("%d%d",&n,&m);
	if (n==1) {printf("1\n"); return 0;}
	for (int i=1;i<=m;i++) scanf("%d%d",&u[i],&d[i]);
	pw[0]=1;
	for (int i=1;i<=n;i++) pw[i]=1ll*pw[i-1]*i%tt;
	for (int j=0;j<(1<<m);j++){
		int s=0,sum=0;
		bool check=0;
		for (int i=1;i<=m;i++)
			if (j&(1<<i-1)) {
				s++,sum+=d[i]-1;
				if (vis[u[i]]==j) {check=1; break;}
				else vis[u[i]]=j;
			}
		if (check==1||sum>n-2) continue;
		ll now=qsm(1ll*(n-s),n-2-sum)*pw[n-2]%tt*qsm(1ll*pw[n-2-sum],tt-2)%tt;
		for (int i=1;i<=m;i++)
		if (j&(1<<i-1)) now=now*qsm(pw[d[i]-1],tt-2)%tt;
		if (s%2==0) ans+=now; else ans-=now;
		ans=(ans%tt+tt)%tt;
	}
	printf("%lld\n",ans);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值