有限背包计数问题 (分类dp)

题目
https://vjudge.net/contest/194413#problem/A
比赛密码:gfoinoip2017

Problem Description

你有一个大小为n的背包,你有n种物品,第i种物品的大小为i,且有i个,求装满这个背包的方案数有多少
两种方案不同当且仅当存在至少一个数i满足第i种物品使用的数量不同

Input

第一行一个正整数n
1<=n<=10^5

Output

一个非负整数表示答案,你需要将答案对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[i1][j]+f[i][j1]f[i1][j(i+1)i]
    <script type="math/tex; mode=display" id="MathJax-Element-212">f_{[i][j]}=f_{[i-1][j]}+f_{[i][j-1]}-f_{[i-1][j-(i+1)*i]}</script>
    是这样一个思路:每新引入一种数(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)。
  • 除了上面分的两类,还有就是既包含 [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,主要是第一种分类和他的方法稍有不同,感觉更直观一些。

猜你喜欢

转载自blog.csdn.net/jackypigpig/article/details/78408964
今日推荐