【VJ】Namomo Camp 3 I-Greenberg Mass Comparison 题解

传送门:Greenberg Mass Comparison
标签:动态规划

题目大意

给出一个正整数n,代表一个集合中有n个不同的数,现在问你该集合有多少种不同的划分。规定集合的划分为:将一个集合分成k个子集,子集间两两交集为空,且所有子集合并后为原集合。规定两个划分A和B是不同的当且仅当:在A划分下属于同一个子集的两个元素在B划分下不再属于同一个子集。
输入:T组数据,每行一个正整数n(1<=n<=100),代表集合大小。
输出:一个正整数,表示答案对1e9+7取模的结果。

算法分析

  • 在思考解法之前,我们得先理解集合划分的本质。如果将其简单地认为是给序列分段,那就大错特错了。我们在初中就学过集合是具有无序性的,子集的概念比子段要更宽泛。如果把集合里的n个元素比作n个不同颜色的小球,那么把集合划分为k个子集就可以近似理解为将n个小球分别放进k个盒子中,且盒子不能留空,也就是组合数学的思想。
  • 考虑到n的数据范围很小如果利用组合数可以利用逆元递推公式实现O(n)计算,但本题并不是纯粹的组合数学,因为n个小球放到k个盒子里的想法看似很对,却忽略了盒子没有特异性这一点。也就是说,假设n=3且k=3,那么[1,2,3]和[3,2,1]这两种划分其实是一样的。如果要用组合数就得进行相当复杂的容斥,我们不妨另寻他路。既然n这么小,我们能不能提前处理出所有的n对应的答案然后离线解决呢?
  • 当然可以。我们假设已知n=i时的答案,那么n=i+1时就相当于往n=i的集合中插入一个数,这样就该往动态规划的方向考虑了。这里我们特殊规定一个新的概念:划分块总数。每种划分都有一个对应的k,也就是有k个划分块,那么划分块总数就是所有的划分情况下的k之和。这样一来我们就很容易得到转移的方法。将一个新的元素x插入原有的集合中时,对于其每个划分,我们可以分两种情况讨论:1、将x插入集合原有的划分块中(原本有k个划分块,所以有k种可能);2、将x单独作为一个划分块。那么n=i+1对应的答案就是n=i时的划分块总数+划分方案数。
  • 看起来本题已经结束了,实则不然。我们还面临一个最困难的问题:划分块总数如何维护。刚刚我们已经进行了分类讨论,对于第一种情况,每插入一种划分块就会多一种方案,而一种方案有k个划分块,所以此时每个原划分的贡献是kk。对于第二种方案,每个划分方案的贡献就是原划分块数加x单独创造的划分块数,也就是k+1。最后要维护所有kk的和需要开一个二维的cnt数组,cnt[i][j]的值代表将大小为i的集合划分为j块的方案数,我们就能得到cnt[i][j]=cnt[i-1][j]*j+cnt[i-1][j-1]的转移。算法总体复杂度为O(n2)。

代码实现

#include <iostream>
using namespace std;
const long long mod=1e9+7;
long long dp[105][2],cnt[105][105];
int main(){
	long long i,j,m,n,T;
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	dp[0][0]=0;
	dp[0][1]=1;
	cnt[0][0]=1;
	for(i=1;i<=100;i++){
		dp[i][1]=(dp[i-1][0]+dp[i-1][1])%mod;
		for(j=1;j<=i-1;j++)
			dp[i][0]=(dp[i][0]+cnt[i-1][j]*j%mod*j%mod)%mod;
		dp[i][0]=(dp[i][0]+dp[i-1][0]+dp[i-1][1])%mod;
		for(j=i;j>=1;j--)
			cnt[i][j]=(cnt[i-1][j]*j+cnt[i-1][j-1])%mod;
	}
	cin>>T;
	while(T--){
		cin>>n;
		cout<<dp[n][1]<<'\n';
	}
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值