题目
https://vjudge.net/contest/194413#problem/A
比赛密码:gfoinoip2017
Problem Description
你有一个大小为n的背包,你有n种物品,第i种物品的大小为i,且有i个,求装满这个背包的方案数有多少
两种方案不同当且仅当存在至少一个数i满足第i种物品使用的数量不同Input
第一行一个正整数n
1<=n<=10^5Output
一个非负整数表示答案,你需要将答案对23333333取模
Samples
Input Output 3 2 23333 16355266 100000 4942409
分析
- 咋一看好像就一背包问题,也的确如此。
- 最暴力的方法就是
f[i][j]
代表用前 i 种数(1~i)能凑成 j 的方案数,然后枚举 i/j/k,代表 i/j/i使用的个数,时间复杂度 O(n^3),显然不行…… 稍微想想(实际上是 BearChild dalao 告诉我的)就可以得出平方算法,
f[i][j]
还是使用上面的意义,可以得出这个:
f[i][j]=f[i−1][j]+f[i][j−1]−f[i−1][j−(i+1)∗i]
是这样一个思路:每新引入一种数(i),利用 1~i 组成 j 的话会有两种情况:f[i-1][j]
:直接只用 i 之前的数组成 j 。f[i][j-i]-f[i-1][j-(i+1)*i]
:利用 1~i 组成 j-i,之后再加一个 i,之所以有一个减数,那是因为题目有限制数字 i 最多用 i 次。在f[i][j-i]
的情况中,可能会出现使用满了 i 个 i 的情况,数目是f[i-1][j-i-i*i]
,这些情况是要减掉的。
这样一来, 时间便成了 O(n^2) ,然而还是并不能过……
- 想到分类,
[1,sqrt(n))
,对于它们中的数,用开头的那个平方算法,可以算过去。O(sqrt(n)*n) - 再想一想,对于剩下的数,都是
sqrt(n)
以上的数 i,(有 n-sqrt(n)个,还是很多的)它们的使用肯定是不会超过 i 个的(即使全选 i 要到 n ,个数也不会超过 i ),那么对于只用[sqrt(n)+1,n]
中的数的答案可以当成完全背包来做,不过平方方法还是会超时。 - 继续优化算法,定义
g[i][j]
为 选 i 个区间[sqrt(n)+1,n]
内的数凑成 j 的方案数。
- 假设已经有了一种 i 个此区间内的数凑成 j 的方案,那么将每个数增加1,可以对后面的 j 做出贡献,或者再增加一个区间中的最小数(
sqrt(n)+1
),也可以对后面的答案做出贡献,而且肯定不会有重复。 - 这样的话,用主动dp,对于当前
g[i][j]
,根据上面两种情况,可以有g[i][j+i]+=g[i][j]
(选出的每个数都+1)和g[i+1][j+nn+1]+=g[i][j]
(再多选一个sqrt(n)+1
) - 显然这样会覆盖所有可能的情况,而且不会重复,因为对于
g[i][j]
,只会被g[i-1][j-nn-1]
和g[i][j-i]
修改,而这两类应该是不会重复的(前面那个得到的方案肯定会有一个 nn+1,而后面那个得到的方案由于都+1,肯定不会有 nn+1)。
- 假设已经有了一种 i 个此区间内的数凑成 j 的方案,那么将每个数增加1,可以对后面的 j 做出贡献,或者再增加一个区间中的最小数(
- 除了上面分的两类,还有就是既包含
[1,sqrt(n)]
中的数,又包含[sqrt(n)+1,n]
中的数的情况,枚举第一个区间的和,再枚举一下 后面那个区间 选几个数,再乘法原理算一下即可。
程序
#include <cstdio>
#include <cmath>
#define Ha 23333333
using namespace std;
typedef long long ll;
int n,nn,i,j,cnt,ans,f[2][100005],g[320][100005];
int main(){
scanf("%d",&n);
nn=sqrt((double)n);
f[0][0]=f[1][0]=1;
for (i=1; i<=nn; i++){ //前 i 种
cnt^=1;
for (j=1; j<=n; j++){ //凑到 j
f[cnt][j]=f[cnt^1][j];
if (j-i>=0) f[cnt][j]=(f[cnt][j]+f[cnt][j-i])%Ha; //注意要判断一下是否越界
if (j-(i+1)*i>=0) f[cnt][j]=((f[cnt][j]-f[cnt^1][j-(i+1)*i])%Ha+Ha)%Ha;
}
}
g[0][0]=1;
for(i=0;i<=nn;i++)
for(j=0;j<=n;j++){ //注意这里也要判断越界。
if(i&&j+i<=n)g[i][j+i]=(g[i][j+i]+g[i][j])%Ha;
if(j+nn+1<=n)g[i+1][j+nn+1]=(g[i+1][j+nn+1]+g[i][j])%Ha;
}
g[1][0]++;
for(int i=0;i<=n;i++)
for(int j=1;j<=nn;j++)
ans=(ans+(ll)f[cnt][i]*g[j][n-i]%Ha)%Ha;
printf("%d",ans);
}
提示
- 注意两种dp中都有些小细节,判断一下是否越界。
- 开数组的时候开大了一点,结果 MLE 了一发……
- 参考此篇文章http://blog.csdn.net/sindardawn/article/details/52926024,主要是第一种分类和他的方法稍有不同,感觉更直观一些。