bzoj2873 光之大陆 Prufer编码+计数

题目链接:戳这里

2873: 光之大陆

Time Limit: 10 Sec  Memory Limit: 256 MB
Submit: 70  Solved: 33
[ Submit][ Status][ Discuss]

Description

在光之大陆的土地上,各种势力盘根错节。来自光之峡谷的精灵,来自黑暗森林的亡灵,来自古老东方的人类共同生活在一起。善于打造装置的矮人,善于发明的侏儒,隐匿于山林的巨人也坚守着属于自己的领土。这些种族之间关系错综复杂,构成了极其庞大的关系网络。大魔法师小P想要研究其中的种族关系。
两个物种之间可以是盟友,也可以不是盟友,如果a1,a2..an满足ai和ai+1是盟友,且an和a1是盟友,则他们构成了一个联盟。
由于光之大陆正处于微妙的和平之中。所以一个合理的物种关系应满足如下条件:
1、对于任意两个物种A,B,都存在一个序列A,a1,a2..an,B,使得任意相邻两个种族是盟友(注意A,B不一定是盟友)。
2、对于任意两个联盟Sa,Sb,都不存在一个物种既参加了联盟Sa,又参加了联盟Sb。
小P想知道,大陆上的N个种族一共有多少种可能的结盟关系,由于结果可能很大,你只需要输出答案mod M的值。
 

Input

一行两个正整数:N,M(含义如题所述)

Output

一个整数:ans表示方案mod M的值

Sample Input

4 1000000

Sample Output

31

HINT

100%测试点保证 n <= 200, m <= 1000000

题解:

先简化一下题意:一个n个节点的无向图,要求联通且只存在简单环,没有两环共点,求有多少种连边方式。

将问题分为两步考虑:(1)n个节点构成k个简单环的方案数。(2)构成的k个环缩点后构成树的方案数。

第二个问题看起来较好解决。

先普及一下Purfer编码:Purfer编码是树的一种编码形式,一个Purfer编码对应唯一的一种树的形态。

编码:每次把度数为1的标号最小的点删掉,输出与它相连的点,直到只剩2个点。
还原:把prufer编码中所有节点编号出现的次数加到每个点的度数里面去(初始度数为1),每次取出度数为1的最小的点和当前purfer所在那一位编号相连,并将两个点度数-1.执行n-2次。最后把剩下的两个度数为1的点相连。

显然一个Purfer编码对应唯一的一颗树的形态。

举个栗子:

我们先把度数为1且编号最小的节点删掉,显然赢先一次删掉2,3,4,即图中的绿色节点,并在Purfer编码中加入其与父节点的连边。

所以在删掉2,3,4后,Purfer编码为[1,1,1]

此时1号节点度数为1,且编号最小,所以应删去1号节点,接着删去6,7,8,号节点,即图中的红色节点。

在删掉1,6,7,8,后,Purfer编码变为[1,1,1,5,5,5,5]

此时只剩下两个点,编码结束。

可以看出,Purfer编码唯一对应一棵树的形态。

同时我们能够推出结论:n个节点能够生成n^(n-2)个Purfer编码,即有n^(n-2)棵不同的生成树。

那么问题2就解决了。

现在来看问题1:我们需要知道n个点形成k个环的方案数。

定义dp[i][k]表示n个点形成k个环的方案数,仅考虑最后一个环的大小,假设为m,我们可以推出转移方程为dp[i][k]=∑dp[i-m][k-1]*C(m-1,n-1)*(m-1)!/2*m。

因为在n个点中选m个点的方案数为C(m-1,n-1),而m个点成环的方案为圆排列/2,m个点成的环向外连一条出边的方案数为m,证毕。

那么问题得解,枚举k,那么ans=∑dp[n][i]*i^(i-2)。

但有个细节:因为两个点不能成环,一个点(缩点后)可以算作环但没有出边,因此在计算中要忽略两个点的情况,单独算一个点(缩点后)的情况。

复杂度O(n^3)。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int read()
{
	char c;int sum=0,f=1;c=getchar();
	while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0' && c<='9'){sum=sum*10+c-'0';c=getchar();}
	return sum*f;
}
int n,m;
int dp[205][205],s[205];
int c[205][205]; 
int main()
{
	n=read();m=read();
	for(int i=0;i<=n;i++)
	c[i][0]=1;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=i;j++)
	c[i][j]=(c[i-1][j]+c[i-1][j-1])%m;
	s[1]=1;s[3]=3;
	for(int i=4;i<=n;i++) s[i]=(s[i-1]*i)%m;
	dp[0][0]=1;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=i;j++)
	for(int k=1;k<=i;k++)
	dp[i][j]=(dp[i][j]+((LL)dp[i-k][j-1]*c[i-1][k-1]*s[k])%m)%m;
	int ans=1;
	for(int i=3;i<=n-1;i++) ans=(ans*i)%m;
	int tmp=1;
	for(int i=2;i<=n;i++)
	{
		ans+=(LL)dp[n][i]*tmp%m;ans%=m;
		tmp=(tmp*n)%m;
	}
	printf("%d\n",ans);
	return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值