传送门: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;
}