动态规划入门(十) 多重背包问题

本文详细介绍了多重背包问题的定义、解决思路及优化方法,并通过具体示例对比了不同背包问题的特点,提供了高效的二进制优化策略及代码实现。

目录

一. 问题描述

二. 基本思路与代码

三. 二进制优化

1. 优化思路

2. 代码实现

四. 为什么不能用完全背包方式优化

 五. 例题分析 


一. 问题描述

        现有 N 不同的物品和一个容量为 V 的背包,已知第 i 件物品最多有 s_i 件,每件物品的体积是 v_i,价值是 w_i。要求在有限的背包容量下,使得装入的物品总价值最大。

        特点:在01背包基础上增加了具体的物品数量限制,但是与完全背包相比每个物品不是无限选择的。这里区别一下三种背包:

(1)01背包:背包有最大容量C,给出n种物品,每种物品仅仅一个,有自己的重量和价值wi和vi,求背包可装下的最大价值

(2)完全背包:背包有最大容量C,给出n种物品,每种物品无限个,有自己的重量和价值wi和vi,求背包可装下的最大价值

(3)多重背包:背包有最大容量C,给出n种物品,每种物品给出数量m,有自己的重量和价值wi和vi,求背包可装下的最大价值

二. 基本思路与代码

        回顾完全背包问题的暴力解法,在背包承重为 j 的前提下,第 i 种物品最多能放 t=j/w[i] 个(这里是整除)。由此可见,对于多重背包问题,只需取 t=\min(s[i],j/w[i]) 。故对完全背包问题的暴力解法做一点简单修改即可求解多重背包问题。

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 1000 + 7;

int v[N], w[N], s[N];
int dp[N][N];

int main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i];

    for(int i = 1; i <= n; i ++){
        for(int j = 0; j <= m; j ++){
            int t = min(s[i],j/w[i]);
            for(int k = 0; k <= t; k ++){
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    }

    cout << dp[n][m] << endl;
    return 0;
}

        或者说,我们考虑把 s[i] 物品拆分为 s[i] 种不同的一件物品,转化为01背包去解题,二者本质上是一样的。其代码示例如下:

#include <bits/stdc++.h>
using namespace std;

int a[10005],b[10005];
int dp[10005];

int main()
{
    int t = 0;
    int n,m;
    cin>>n>>m;
    while(n--)
    {
        int w,v,s;
        cin>>v>>w>>s;
        while(s--)
        {
            a[++t]=v;
            b[t]=w;
        }//死拆,把多重背包拆成01背包
    }
    for(int i=1;i<=t;i++){
        for(int j=m;j>=a[i];j--){
            dp[j]=max(dp[j-a[i]]+b[i],dp[j]);//直接套01背包的板子
        }
    }
    cout<< dp[m] <<endl;
    return 0;
}

三. 二进制优化

1. 优化思路

        对于多重背包我们完全可以把他转化为01背包,每一种都分成一个个的来当做01背包处理。但是一个个分的话效率太低,这里考虑二进制处理。这里引入两个定理:

  • 任何一个数字 n 都可以拆为 1 + 2^1 + 2^2 + 2^3 + ...... + 2^k + C,(C=n-\sum_{0}^{k}2^i) 的形式
  • 1,2^1 , 2^2 , 2^3 , ....,2^k 可以组合为 [1,2^{k+1}) 之间的任意一个数字。比如我们可以用1,2,4,8,…,512 (2^9) 来组合成0~1023内的任意一个数字,至多枚举10次

        则根据以上可知,1 , 2^1 , 2^2 , 2^3 , ...... , 2^k , C(C=n-\sum_{0}^{k}2^i) 就可以组合为 1 - n 内的任意一个数字。我们之前暴力求解时将物品拆分为一个个遍历的目的就是组合为 1 - n 内的任意一种可能来取最值,那么目前有了二进制拆分这种更少的拆分次数就可以实现所有数字组合,不仅加快了效率而且这些数字也可以组合为 1 - n 间的任意一个数字表示任意一个数量,两全其美。

        举个更形象的例子:要求在一堆苹果选出n个苹果。我们传统的思维是一个一个地去选,选够n个苹果就停止。这样选择的次数就是n次;如果将这一堆苹果分别按照 1,2,4,...,256,512 分到10个箱子里,那么由于任何一个数字 x∈[0,1023] (第11个箱子才能取到1024) 都可以从这10个箱子里的苹果数量表示出来,这样选择的次数就是 ≤10次。

    int k = 1;
    while(k<m){
        ZeroOnePack(w*k,v*k);//01背包(组合物品重量为w*k,价值为v*k)
        m-=k;
        k*=2;
    }
    ZeroOnePack(w*m,v*m);

2. 代码实现

(1)多重背包转化为01背包

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 25000;
int f[N], v[N], w[N];
int n, m;

int main(){
    cin >> n >> m;

    //将每种物品根据物件个数进行打包
    int cnt = 0;
    for(int i = 1; i <= n; i ++){
        int a, b, s;
        cin >> a >> b >> s;

        int k = 1;
        while(k <= s){
            cnt ++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if(s > 0){
            cnt ++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }

    }

    //多重背包转化为01背包问题
    for(int i = 1; i <= cnt; i ++){
        for(int j = m; j >= v[i]; j --){
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }

    cout << f[m] << endl;

    return 0;
}

(2)拆分为完全背包和01背包

  • 若w * k >= C(该物品总重量>=背包总重):可以看作为完全背包处理
  • 若w * k < C  : 当做01背包处理
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int maxn = 10000 + 5;
int best[maxn];
int n,C;
void CompletePack(int w,int v)//完全背包(顺序)
{
    for(int i = w;i<=C;i++){
        best[i] = best[i]>best[i - w]+v?best[i]:best[i-w]+v;
    }
}
void ZeroOnePack(int w,int v)//01背包(逆序)
{
    for(int i = C;i>=w;i--){
        best[i] = max(best[i],best[i-w]+v);
    }
}
void MultiplePack(int w,int v,int m)
{
    if(w*m>=C){//当做完全背包处理
        CompletePack(w,v);
        return;
    }
    int k = 1;//分解为01背包(二进制拆分)
    while(k<m){
        ZeroOnePack(w*k,v*k);
        m-=k;
        k*=2;
    }
    ZeroOnePack(w*m,v*m);//最后剩余
    return;
}
int main()
{
    while(scanf("%d%d",&n,&C)!=EOF&&(n||C)){
        memset(best,0,sizeof(best));
        int w,v,m;
        for(int i = 0;i<n;i++){
            scanf("%d%d%d",&w,&v,&m);
            MultiplePack(w,v,m);
        }
        printf("%d\n",best[C]);
    }
    return 0;
}

四. 为什么不能用完全背包方式优化

 五. 例题分析 

(1) HDU - 2844 Coins

        给你n个钱币的数量和价值, 给你一个m ,让求这些钱币能组合成多少种不同的<=m的价值。注意:这里要求支付价钱时,组合是正好的不用找零!

        分析:乍一眼以为要从规划数量入手,那样就用不上条件了。其实留意一下题意里的注意,我们还是dp每种价格下最大的组合价值,如果这个是一种组合方式的话 dp[i] == i 了(因为dp[i]为<=i的最大价值),所以我们dp以后在查找一下有多少dp[i] == i即可!注意这里钱币的重量 = 钱币的价值。

#include <iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 100 + 10;
int w[maxn],v[maxn];
int dp[maxn*1000];
int n,m;
void CompletePack(int W,int V)
{
    for(int i = W;i<=m;i++){
        dp[i] = max(dp[i],dp[i - W] + V);
    }
}
void ZeroOnePack(int W,int V)
{
    for(int i = m;i>=W;i--){
        dp[i] = max(dp[i],dp[i - W] + V);
    }
}
void MultiplePack(int wi,int vi)
{
    if(wi*vi>=m){
        CompletePack(vi,vi);
        return;
    }
    int k = 1;
    while(k<wi){
        ZeroOnePack(k*vi,k*vi);
        wi-=k;
        k*=2;
    }
    ZeroOnePack(wi*vi,vi*wi);
    return;
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF&&(n||m)){
        memset(dp,0,sizeof(dp));
        for(int i = 0;i<n;i++){
            scanf("%d",&v[i]);
        }
        for(int j = 0;j<n;j++){
            scanf("%d",&w[j]);
        }
        for(int i = 0;i<n;i++){
            MultiplePack(w[i],v[i]);
        }
        int cnt = 0;
        //cout<<m<<endl;
        for(int i = 1;i<=m;i++){
            //cout<<dp[i]<<endl;
            if(dp[i]==i)cnt++;
        }
        printf("%d\n",cnt);
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿阿阿安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值