POJ3046 多重集组合数 dp+前缀和优化+滚动数组 (包含类似优化的小总结)

题意

  • 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值