【51nod 1597】有限背包计数问题 题目 思路 代码

题目链接:http://www.51nod.com/Challenge/Problem.html#problemId=1597
你有一个大小为 (n) 的背包,你有 (n) 种物品,第 (i) 种物品的大小为 (i),且有 (i) 个,求装满这个背包的方案数有多少。答案对 23333333 取模。
两种方案不同当且仅当存在至少一个数 (i) 满足第 (i) 种物品使用的数量不同。
(nleq 10^5)

思路

这题看上去就非常根号分治。考虑分别求出物品编号小于 (sqrt{n}) 和大于等于 (sqrt{n}) 时的背包,然后合并。
首先来看小于 (sqrt{n})。此时每个物品是有不能取超过 (i) 个的限制的。由于物品数只有 (O(sqrt{n})),所以可以直接设 (f[i][j]) 表示前 (i) 个物品,使用了 (j) 的容量的方案数。
然后有转移

[f[i][j]=sum^{i}_{k=0}f[i-1][j-ki] ]

就是枚举用多少个物品 (i)。这个玩意前缀和优化一下就好了。
然后再看编号大于等于 (sqrt{n}) 的物品,我么发现这些物品一定是取不到上限的,所以其实等价于一个完全背包。
有一个很经典的 trick:假设我们有一个序列 (a),我们可以通过如下操作构成这个序列:添加一个大小为 (1) 的物品,或者将所有物品大小加一。不难发现这个操作顺序与序列 (a) 是一一对应的。
同理,设 (g[i][j]) 表示选择了 (i) 个编号超过 (sqrt{n}) 的物品,容量之和为 (j) 的方案数。那么考虑添加一个大小为 (sqrt{n}) 的物品,或者将所有物品大小加一,有

[g[i][j]=g[i][j-i]+g[i-1][j-sqrt{n}] ]

最后合并背包即可。注意需要滚动数组。
时间复杂度 (O(nsqrt{n}))

代码

#include <bits/stdc++.h>
using namespace std;

const int N=100010,M=320,MOD=23333333;
int n,ans,f[2][N],g[2][N],h[N];

int main()
{
	scanf("%d",&n);
	f[0][0]=g[0][0]=1;
	for (int i=1;i<M;i++)
	{
		int id=i&1;
		memset(f[id],0,sizeof(f[id]));
		for (int j=0;j<=n;j++)
		{
			h[j]=(f[id^1][j]+((j>=i) ? h[j-i] : 0))%MOD;
			f[id][j]=(h[j]-((j>=i*(i+1)) ? h[j-i*(i+1)] : 0))%MOD;
		}
	}		
	memset(h,0,sizeof(h));
	for (int i=1;i<=M;i++)
	{
		int id=i&1;
		memset(g[id],0,sizeof(g[id]));
		for (int j=0;j<=n;j++)
		{
			if (j>=i) g[id][j]=(g[id][j]+g[id][j-i])%MOD;
			if (j>=M) g[id][j]=(g[id][j]+g[id^1][j-M])%MOD;
			h[j]=(h[j]+g[id][j])%MOD;
		}
	}
	h[0]=1;
	for (int i=0;i<=n;i++)
		ans=(ans+1LL*f[1][i]*h[n-i])%MOD;
	printf("%d",(ans%MOD+MOD)%MOD);
	return 0;
}