DP 经典问题(六)多重部分和问题

问题描述:

有n种不同大小的数字ai,每种各mi个,判断是否可以从这些数字之中选出若干使它们的和恰好为K。(1 <=n <= 100 , 1 <= ai,mi <=100000 , 1 <= K <= 100000)
输入:

n = 3 ,K = 17
a = { 3 , 5 , 8}
m = { 3 , 2 , 2}

输出

Yes(3*3+8=17}

分析:

  • 1.刻画一个最优解的结构特征:
    定义dp[i+1][j]为前 i 个数是否能加和成 j

  • 2.递归地定义最优解的值:
    为了让前 i 个数加和成j,那么前 i-1个数就需要加和成 j , j - ai , …. ,j - mi*ai中的某一种。
    由此,递推关系如下:
    dp[i+1][j]=(0 <= k <=mi且k*ai <= j时存在使dp[i][j-k*a]为真的k)

  • 3.计算最优解的值,采用自底向上的递推法。
    代码如下:

#include<cstdio>
using namespace std;
const int maxn = 100;
int n,K;
int a[maxn],m[maxn];
bool dp[maxn][maxn];//dp[i+1][j]:用前i种数字是否能加和为j

void solve()
{
    dp[0][0] = true;//没有数字加和当然为0了
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<=K;j++)
        {
            for(int k=0;k<=m[i]&&k*a[i]<=j;k++)
            {
                dp[i+1][j] |= dp[i][j-k*a[i]];//"|="为位操作运算符——位或 
            }
        }
    }
    if(dp[n][K]) printf("Yes\n");
    else printf("No\n");
}

int main()
{
    scanf("%d%d",&n,&K);
    for(int i=0;i<n;i++)
    scanf("%d",&a[i]);
    for(int i=0;i<n;i++)
    scanf("%d",&m[i]);
    solve();
    return 0;
}

这个算法时间复杂度为O(K∑imi),所以还需要优化

优化:

  • 1.刻画一个最优解的结构特征:
    重新定义dp[i+1][j]为前i种数加和得到 j 时第 i 种数最多还能剩余多少个(不能加和得到 i 的情况为 -1)

  • 2.递归地定义最优解的值:
    如果前 i-1个数加和得到 j 的话,那么第 i 个数就不用加了,就剩下mi个。
    如果前 i 种数加和出 j - ai时第 i 种数还剩下k的话,用这 i 种数加和 j 时第 i 种数就能剩下k - 1个。
    dp[i+1][j]=

    • 1.mi (dp[i][j]>=0) ;
    • 2.-1 (j < ai 或者dp[i+1][j-ai]<=0) ;
    • 3.dp[i+1][j-ai]-1 (其他);
  • 3.计算最优解的值,采用自底向上的递推法。
    代码如下:

#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 100000+10;
int dp[maxn],a[maxn],m[maxn];//dp[i+1][j]为用前i种数加和得到j时第i种数最多能剩余多少个 

int main()
{
    int n,K;
    scanf("%d%d",&n,&K);
    for(int i=0; i<n; i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=0; i<n; i++)
    {
        scanf("%d",&m[i]);  
    }
    memset(dp,-1,sizeof(dp));
    dp[0] = 0;
    for(int i = 0; i<n; i++)
    {
        for(int j=0; j<=K; j++)
        {
            if(dp[j] >= 0)
            {
                dp[j] = m[i];//如果前i-1个数加和能得到j的话,第i个数就可以留下mi个 
            }else if(j < a[i] || dp[j - a[i]]<=0)
            {
                dp[j] = -1;
            }else{
                dp[j] = dp[j-a[i]] - 1;
            } 
        }
    }
    if(dp[K]>=0) printf("Yes\n");
    else printf("No\n");
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值