题意
- T种数,每种有a[t]个,总共有A个数。问你取其中X个数作为子集,有多少种这样的子集。计算L<=X<=H的子集数之和,结果mod 1000000
- 1<=T<=1000, 1<=a[t] <= 100,即1 <= A <= 100000, L<=H<=A
思路
- 基本想法:可以类比为把求max改为求sum的多重背包,所以基本的递推关系很好写,dp(i,j) = sum dp(i-1)(j-k) 其中k = 0~a[i] ,dp(i,j)表示前i种数,其中j个数作为子集的取法。
- 很明显,这样做会超时、超空间,超空间好说,是基本的滚动数组。我们主要解决超时的问题。
- 前缀和优化,我们把状态改一下。dp(i,j)表示前i种数,其中0~j个数作为子集时的取法。这样dp(i,j) = dp(i-1, j) - dp(i-1, j-a[i]-1) + dp(i,j-1) 时间复杂度的问题就解决了。
- 滚动数组,我们可以看到,每次更新i时,我们要用到i-1时的 j 和 j-a[i]-1,那么应该倒着更新j。可是这样的话,在更新dp(i,j)时,dp(i,j-1)还没有更新出来,无法更新dp(i,j)。这里我们有两种解决方案,一是,保存两个dp(j),这样我们通过用i&1找到用哪个dp,然后正向更新dp(j),另一种是,保存一个dp(j),做两次循环更新它。第一次做,dp(i,j) = dp(i-1, j) - dp(i-1, j-a[i]-1),求出不带前缀和的,第二次专门求前缀和。
一些小总结
- 这种前缀和优化适用于每次求sum时,不用再乘一个和j相关的系数时,比如dp (i,j) = sum dp(i-1)(j-k) * c(j) 这时就无法用前缀和来优化了。
- 滚动数组的第二种方案是我这次突然想到的,我觉得也挺好的,把问题差分了,相当于加入了一个辅助的数组,一个保存前i种数,其中j个数作为子集的取法,另一个保存前i种数,其中0~j个数作为子集时的取法,然后再更新时比较好写。
- 另外,我想不是求sum的问题时,比如max和min也都可以用类似的想法优化。也是当没有更多相关的系数时,我们可以用RMQ问题或者线段树作为一个辅助数组,来优化时间复杂度。
实现
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
int T,n,L,H;
const int mod = 1000000;
int dp[100005];
int a[1005];
int main(){
int i,j;
cin>>T>>n>>L>>H;
for (int i=0;i<n;i++){
int tmp;
scanf("%d",&tmp);
a[tmp]++;
}
for (int i=0;i<=H;i++)
dp[i] = 1;
for (int i=1;i<=T;i++){
for (int j=H;j>=0;j--){
if (j > a[i]){
dp[j] = (dp[j] - dp[j-a[i]-1] + mod) % mod;
}
}
for (int j=1;j<=H;j++){
dp[j] = (dp[j] + dp[j-1]) % mod;
}
}
cout << (dp[H] - dp[L-1] + mod) % mod<< '\n';
return 0;
}