7.2.背包DP

背包DP

在C++中,背包动态规划(Knapsack DP) 是解决资源分配类问题的核心算法范式,尤其在处理物品选择与容量限制的组合优化问题时表现优异。以下是针对不同背包类型的详细解析与代码实现:


一、背包DP问题分类

类型特点经典问题场景
0-1背包每件物品 只能选0或1次物品不可分割(如金条)
完全背包每件物品 可选无限次硬币兑换(面额无限供应)
多重背包每件物品 最多选k次有限库存的商品采购
分组背包物品分组,每组选一件课程选修(多门课选其一)

二、0-1背包问题

问题描述

给定物品重量 weights[] 和价值 values[],背包容量 W,求能装入的最大总价值。

状态定义
  • 基础二维DPdp[i][w] 表示前 i 个物品在容量 w 下的最大价值
  • 优化一维DPdp[w] 表示容量 w 下的最大价值(空间压缩)
状态转移方程
  • 不选第 i 件:dp[i][w] = dp[i-1][w]
  • 选第 i 件:dp[i][w] = dp[i-1][w - weights[i]] + values[i]
  • 综合:dp[i][w] = max(dp[i-1][w], dp[i-1][w - weights[i]] + values[i])
C++实现
// 二维DP(清晰但空间占用大)
int knapsack_01_2D(vector<int>& weights, vector<int>& values, int W) {
    int n = weights.size();
    vector<vector<int>> dp(n+1, vector<int>(W+1, 0));
    
    for (int i = 1; i <= n; ++i) {
        for (int w = 0; w <= W; ++w) {
            if (w < weights[i-1]) {
                dp[i][w] = dp[i-1][w];
            } else {
                dp[i][w] = max(dp[i-1][w], 
                              dp[i-1][w - weights[i-1]] + values[i-1]);
            }
        }
    }
    return dp[n][W];
}

// 一维DP(空间优化,必须逆序遍历容量)
int knapsack_01_1D(vector<int>& weights, vector<int>& values, int W) {
    vector<int> dp(W + 1, 0);
    for (int i = 0; i < weights.size(); ++i) {
        for (int w = W; w >= weights[i]; --w) { // 逆向更新
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }
    return dp[W];
}
关键点
  • 逆序遍历容量:确保每个物品只被计算一次
  • 索引对应weights[i-1] 对应二维版的第 i 个物品(一维版直接 weights[i]

三、完全背包问题

问题描述

物品可无限次选取,求装满背包的最大价值。

状态转移方程

dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
(与0-1背包的唯一区别是 正序遍历容量

C++实现
int knapsack_unbounded(vector<int>& weights, vector<int>& values, int W) {
    vector<int> dp(W + 1, 0);
    for (int i = 0; i < weights.size(); ++i) {
        for (int w = weights[i]; w <= W; ++w) { // 正序遍历
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }
    return dp[W];
}
应用场景
  • 零钱兑换问题(LeetCode 322)
  • 凑满容量的最小物品数(LeetCode 279)

四、多重背包问题

问题描述

每个物品有数量限制 counts[i],最多选 counts[i] 次。

优化方法

将多重背包转换为 0-1背包 + 二进制拆分,减少物品数量:

  • 例如:将13个物品拆分为 1, 2, 4, 6(组合可表示1~13)
C++实现
int knapsack_multiple(vector<int>& weights, vector<int>& values, 
                      vector<int>& counts, int W) {
    // 二进制拆分预处理
    vector<int> new_weights, new_values;
    for (int i = 0; i < weights.size(); ++i) {
        int k = 1;
        while (counts[i] > 0) {
            int amount = min(k, counts[i]);
            new_weights.push_back(weights[i] * amount);
            new_values.push_back(values[i] * amount);
            counts[i] -= amount;
            k *= 2;
        }
    }
    
    // 转换为0-1背包
    vector<int> dp(W + 1, 0);
    for (int i = 0; i < new_weights.size(); ++i) {
        for (int w = W; w >= new_weights[i]; --w) {
            dp[w] = max(dp[w], dp[w - new_weights[i]] + new_values[i]);
        }
    }
    return dp[W];
}

五、分组背包问题

问题描述

物品被分为多组,每组只能选一个物品。

状态转移方程

dp[w] = max{ 不选该组 / 选该组第k个物品 }

C++实现
int group_knapsack(vector<vector<pair<int, int>>>& groups, int W) {
    vector<int> dp(W + 1, 0);
    for (auto& group : groups) { // 遍历每组
        for (int w = W; w >= 0; --w) { // 逆序遍历容量
            for (auto& item : group) { // 遍历组内物品
                int weight = item.first, value = item.second;
                if (w >= weight) {
                    dp[w] = max(dp[w], dp[w - weight] + value);
                }
            }
        }
    }
    return dp[W];
}

六、常见问题与调试技巧

1. 初始化陷阱
  • 要求恰好装满背包时:dp[0] = 0,其他初始化为 -INF
  • 不要求装满时:全部初始化为 0
2. 遍历顺序总结
背包类型容量遍历顺序物品遍历顺序
0-1背包逆序先物品后容量
完全背包正序先物品后容量
分组背包逆序先组后容量再物品
3. 测试用例设计
  • 边界测试:容量为0、物品重量为0
  • 极端情况:所有物品重量超过容量
  • 完全覆盖:验证状态转移的所有分支

七、典型例题与代码

1. 分割等和子集(LeetCode 416)

转化为0-1背包:找是否能凑出 sum/2

bool canPartition(vector<int>& nums) {
    int sum = accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    int target = sum / 2;
    
    vector<bool> dp(target + 1, false);
    dp[0] = true;
    for (int num : nums) {
        for (int w = target; w >= num; --w) {
            dp[w] = dp[w] || dp[w - num];
        }
    }
    return dp[target];
}
2. 零钱兑换 II(LeetCode 518)

完全背包求组合数:

int change(int amount, vector<int>& coins) {
    vector<int> dp(amount + 1, 0);
    dp[0] = 1;
    for (int coin : coins) { // 先遍历物品(保证组合数)
        for (int w = coin; w <= amount; ++w) {
            dp[w] += dp[w - coin];
        }
    }
    return dp[amount];
}

八、性能优化进阶

  1. 滚动数组:将二维DP压缩为两个一维数组交替使用
  2. 单调队列优化:适用于特定多重背包问题
  3. 位运算优化:当价值较小时,用bitset加速状态转移

掌握背包DP的关键在于 理解物品选择逻辑与遍历顺序的关系,并通过大量练习熟悉不同变种的解题模式。建议从标准0-1背包入手,逐步扩展到更复杂的变种。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

赵鑫亿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值