牛客题解 | 04.03_多重背包

完全背包# 多重背包

问题描述

n n n 个物品,每个物品有重量 w i w_i wi、价值 v i v_i vi 和数量限制 s i s_i si。现在有一个容量为 W W W 的背包,每个物品最多选择 s i s_i si 次,求解在不超过背包容量的情况下,能够装入物品的最大价值。

与其他背包的区别

  1. 01背包:每个物品最多选择1次
  2. 完全背包:每个物品可以选择无限次
  3. 多重背包:每个物品可以选择 [ 0 , s i ] [0,s_i] [0,si]

基本解法

状态定义

  • d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考虑前 i i i 个物品,背包容量为 j j j 时能获得的最大价值

状态转移方程

dp[i][j] = max(dp[i-1][j-k*w[i]] + k*v[i])
其中 k∈[0,s[i]] 且 k*w[i] ≤ j

二进制优化

将每个物品的数量 s i s_i si 进行二进制拆分,转化为01背包问题。

例如:数量为13可以拆分为1,2,4,6,因为13=1+2+4+6

C++ 实现(二进制优化)

int multipleKnapsack(vector<int>& w, vector<int>& v, vector<int>& s, int W) {
    int n = w.size();
    vector<int> dp(W + 1, 0);
    
    for (int i = 0; i < n; i++) {
        // 二进制拆分
        int num = s[i];
        for (int k = 1; num > 0; k <<= 1) {
            int amount = min(k, num);
            num -= amount;
            // 01背包过程
            for (int j = W; j >= w[i] * amount; j--) {
                dp[j] = max(dp[j], dp[j - w[i] * amount] + v[i] * amount);
            }
        }
    }
    
    return dp[W];
}

Java 实现

class Solution {
    public int multipleKnapsack(int[] w, int[] v, int[] s, int W) {
        int n = w.length;
        int[] dp = new int[W + 1];
        
        for (int i = 0; i < n; i++) {
            int num = s[i];
            for (int k = 1; num > 0; k <<= 1) {
                int amount = Math.min(k, num);
                num -= amount;
                for (int j = W; j >= w[i] * amount; j--) {
                    dp[j] = Math.max(dp[j], dp[j - w[i] * amount] + v[i] * amount);
                }
            }
        }
        
        return dp[W];
    }
}

Python 实现

def multipleKnapsack(w: List[int], v: List[int], s: List[int], W: int) -> int:
    n = len(w)
    dp = [0] * (W + 1)
    
    for i in range(n):
        num = s[i]
        k = 1
        while num > 0:
            amount = min(k, num)
            num -= amount
            for j in range(W, w[i] * amount - 1, -1):
                dp[j] = max(dp[j], dp[j - w[i] * amount] + v[i] * amount)
            k <<= 1
    
    return dp[W]

时间复杂度分析

  • 朴素解法: O ( n ⋅ W ⋅ ∑ s i ) \mathcal{O}(n \cdot W \cdot \sum s_i) O(nWsi)
  • 二进制优化: O ( n ⋅ W ⋅ log ⁡ max ⁡ ( s i ) ) \mathcal{O}(n \cdot W \cdot \log \max(s_i)) O(nWlogmax(si))
  • 空间复杂度: O ( W ) \mathcal{O}(W) O(W)

优化方法

  1. 二进制拆分:

    • 将每个物品拆分成二进制数量
    • 显著减少状态转移的次数
  2. 单调队列优化:

    • 对于较大的数量可以使用单调队列
    • 时间复杂度可以优化到 O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(nW)

应用场景

  1. 库存管理问题
  2. 资源分配规划
  3. 生产计划制定
  4. 投资组合优化
  5. 商品促销策略

注意事项

  1. 数量限制的处理
  2. 二进制拆分的实现
  3. 整数溢出问题
  4. 内存使用限制
  5. 特殊情况处理

经典例题

  1. 【模板】多重背包
  2. 称砝码

问题描述

n n n 个物品,每个物品有重量 w i w_i wi 和价值 v i v_i vi。现在有一个容量为 W W W 的背包,每个物品可以选择任意次(无限次),求解在不超过背包容量的情况下,能够装入物品的最大价值。

与01背包的区别

  1. 01背包:每个物品最多选择一次
  2. 完全背包:每个物品可以选择无限次

动态规划解法

状态定义

  • d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考虑前 i i i 个物品,背包容量为 j j j 时能获得的最大价值

状态转移方程

dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i])  if j >= w[i]
dp[i][j] = dp[i-1][j]                              if j < w[i]

注意:与01背包的区别是 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][jw[i]] 而不是 d p [ i − 1 ] [ j − w [ i ] ] dp[i-1][j-w[i]] dp[i1][jw[i]]

代码实现

C++ 实现(二维)

int completeKnapsack(vector<int>& w, vector<int>& v, int W) {
    int n = w.size();
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
    
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= W; j++) {
            if (j >= w[i-1]) {
                dp[i][j] = max(dp[i-1][j], dp[i][j-w[i-1]] + v[i-1]);
            } else {
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    
    return dp[n][W];
}

C++ 实现(一维优化)

int completeKnapsack(vector<int>& w, vector<int>& v, int W) {
    int n = w.size();
    vector<int> dp(W + 1, 0);
    
    for (int i = 0; i < n; i++) {
        for (int j = w[i]; j <= W; j++) {
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
        }
    }
    
    return dp[W];
}

Java 实现

class Solution {
    public int completeKnapsack(int[] w, int[] v, int W) {
        int n = w.length;
        int[] dp = new int[W + 1];
        
        for (int i = 0; i < n; i++) {
            for (int j = w[i]; j <= W; j++) {
                dp[j] = Math.max(dp[j], dp[j-w[i]] + v[i]);
            }
        }
        
        return dp[W];
    }
}

Python 实现

def completeKnapsack(w: List[int], v: List[int], W: int) -> int:
    n = len(w)
    dp = [0] * (W + 1)
    
    for i in range(n):
        for j in range(w[i], W + 1):
            dp[j] = max(dp[j], dp[j-w[i]] + v[i])
    
    return dp[W]

与01背包的代码区别

  1. 状态转移时使用当前行的状态
  2. 容量的遍历方向改为正序
  3. 可以直接从物品重量开始遍历

时间复杂度分析

  • 时间复杂度: O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(nW)
  • 空间复杂度:
    • 二维实现: O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(nW)
    • 一维实现: O ( W ) \mathcal{O}(W) O(W)

常见变形

  1. 求恰好装满背包的最大价值
  2. 求最少需要多少个物品装满背包
  3. 求装满背包的方案数
  4. 求满足条件的最小代价

应用场景

  1. 零钱兑换问题
  2. 物资补给规划
  3. 资源重复利用
  4. 生产计划制定
  5. 库存补货策略

注意事项

  1. 遍历顺序的区别
  2. 状态转移的依赖关系
  3. 初始化的处理
  4. 特殊情况的考虑
  5. 数据范围的限制

经典例题

  1. 【模板】完全背包
  2. 最少的完全平方数
  3. 兑换零钱
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值