C++ 背包DP解析

目录

背包DP:从原理到实战的深度解析(对话版)

引言

背包问题是动态规划(DP)中最经典、最基础的一类问题,几乎是所有算法学习者的"必修课"。它看似简单(往背包里装物品,追求价值最大化),却衍生出01背包、完全背包、多重背包等多个变种,核心思想贯穿了整个DP体系。本文以"新手提问+导师解答"的对话形式,从最基础的01背包入手,逐步拆解各类背包问题的原理、状态转移和代码实现,让你彻底吃透背包DP。

一、初识背包DP:核心思想是什么?

新手 :导师您好!我刚接触动态规划,看到"背包DP"一头雾水------它到底是解决什么问题的?核心思路是什么?
导师 :别急,咱们先从最朴素的场景理解:

假设你有一个容量为C的背包,还有n个物品,每个物品有两个属性:重量w[i]和价值v[i]。背包DP的核心问题是:如何选择物品装入背包,使得总重量不超过背包容量,且总价值最大

而DP解决问题的核心是"拆分子问题+记录状态+状态转移",背包DP也不例外:

  1. 状态定义 :用dp[i][j]表示"前i个物品,装入容量为j的背包时,能获得的最大价值";
  2. 状态转移 :对第i个物品,只有两种选择------装或不装,取两种选择的最大值;
  3. 初始状态:没有物品或背包容量为0时,价值都是0。

这就是背包DP的核心框架,所有变种都是在这个框架上调整。

二、基础中的基础:01背包(每个物品最多选1次)

新手 :01背包是最基础的吧?为什么叫"01"?
导师:"01"对应"选或不选"------每个物品只有两种状态:0(不选)、1(选),所以叫01背包。咱们先讲二维数组版(好理解),再讲一维优化版(实战常用)。

(一)二维数组实现01背包

导师:先明确问题:

  • 物品数量n,背包容量C
  • 物品数组:w[1..n](重量)、v[1..n](价值);
  • 求最大价值。
1. 状态定义

dp[i][j]:前i个物品,背包容量为j时的最大价值。

2. 状态转移

对第i个物品:

  • 不选:dp[i][j] = dp[i-1][j](价值和前i-1个物品一样);
  • 选:前提是j >= w[i],此时dp[i][j] = dp[i-1][j - w[i]] + v[i](容量减去当前物品重量,价值加上当前物品价值);
  • 最终取两者最大值:dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])j >= w[i]时);
  • 如果j < w[i],只能不选:dp[i][j] = dp[i-1][j]
3. 初始状态
  • dp[0][j] = 0(0个物品,任何容量价值都是0);
  • dp[i][0] = 0(背包容量为0,装不了任何物品,价值0)。
4. 代码实现(C++)
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 01背包:二维数组版
int zeroOnePack(int n, int C, vector<int>& w, vector<int>& v) {
    // dp[i][j]:前i个物品,容量j的最大价值
    vector<vector<int>> dp(n + 1, vector<int>(C + 1, 0));
    
    for (int i = 1; i <= n; ++i) { // 遍历每个物品
        for (int j = 1; j <= C; ++j) { // 遍历每个容量
            if (j < w[i]) {
                // 容量不够,只能不选第i个物品
                dp[i][j] = dp[i-1][j];
            } else {
                // 选或不选,取最大值
                dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i]);
            }
        }
    }
    return dp[n][C]; // 最终答案:前n个物品,容量C的最大价值
}

int main() {
    // 测试案例:n=3个物品,背包容量C=5
    // 物品1:w=1, v=2;物品2:w=2, v=3;物品3:w=3, v=4
    int n = 3, C = 5;
    vector<int> w = {0, 1, 2, 3}; // 下标从1开始,方便理解
    vector<int> v = {0, 2, 3, 4};
    
    int maxVal = zeroOnePack(n, C, w, v);
    cout << "01背包最大价值:" << maxVal << endl; // 输出7(选物品1+2+3?不,1+2重量3,价值5;1+3重量4,价值6;2+3重量5,价值7)
    return 0;
}

新手 :这段代码我能看懂!但二维数组的空间复杂度是O(n*C),如果n和C很大(比如1e4),空间会不够吧?
导师 :没错!所以实战中都会用一维数组优化,把空间复杂度降到O©。

(二)一维数组优化01背包

导师 :核心思路是"滚动数组"------观察状态转移方程,dp[i][j]只依赖dp[i-1][...](上一层的状态),所以可以用一维数组dp[j]代替二维数组,只需注意容量遍历顺序必须是从后往前(避免重复选同一个物品)。

1. 状态定义

dp[j]:当前背包容量为j时的最大价值(等价于二维的dp[i][j])。

2. 状态转移

dp[j] = max(dp[j], dp[j - w[i]] + v[i])j >= w[i])。

3. 关键:容量从后往前遍历

如果从前往后遍历(比如j从1到C),会导致同一个物品被多次选择(比如j=w[i]时选了,j=2*w[i]时又选一次),违背01背包"每个物品只能选一次"的规则;从后往前遍历,能保证每个物品只被考虑一次。

4. 代码实现(C++)
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 01背包:一维数组优化版(空间O(C))
int zeroOnePackOpt(int n, int C, vector<int>& w, vector<int>& v) {
    vector<int> dp(C + 1, 0); // 一维dp数组,初始全0
    
    for (int i = 1; i <= n; ++i) { // 遍历每个物品
        // 容量从后往前遍历,避免重复选
        for (int j = C; j >= w[i]; --j) {
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    return dp[C];
}

int main() {
    int n = 3, C = 5;
    vector<int> w = {0, 1, 2, 3};
    vector<int> v = {0, 2, 3, 4};
    
    int maxVal = zeroOnePackOpt(n, C, w, v);
    cout << "优化版01背包最大价值:" << maxVal << endl; // 输出7
    return 0;
}

新手:太妙了!空间直接省了一半还多。那接下来该讲完全背包了吧?

三、经典变种:完全背包(每个物品可选无限次)

新手 :完全背包和01背包的区别就是"物品能选无限次"?那状态转移该怎么改?
导师:没错!完全背包的核心是"每个物品可以选0次、1次、2次......直到超过背包容量"。咱们还是先讲思路,再给代码。

(一)核心思路

对比01背包:

  • 01背包:容量从后往前遍历(避免重复选);
  • 完全背包:容量从前往后遍历(允许重复选)。

为什么?因为完全背包允许同一个物品选多次,从前往后遍历,dp[j - w[i]]已经是选过当前物品后的状态,再加上v[i],就相当于多选了一次。

状态转移

dp[j] = max(dp[j], dp[j - w[i]] + v[i])(和01背包一样),但j从w[i]到C遍历

(二)代码实现(C++)

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 完全背包:一维数组版(空间O(C))
int completePack(int n, int C, vector<int>& w, vector<int>& v) {
    vector<int> dp(C + 1, 0);
    
    for (int i = 1; i <= n; ++i) { // 遍历每个物品
        // 容量从前往后遍历,允许重复选
        for (int j = w[i]; j <= C; ++j) {
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    return dp[C];
}

int main() {
    // 测试案例:n=2个物品,背包容量C=5
    // 物品1:w=1, v=2;物品2:w=2, v=3
    int n = 2, C = 5;
    vector<int> w = {0, 1, 2};
    vector<int> v = {0, 2, 3};
    
    int maxVal = completePack(n, C, w, v);
    cout << "完全背包最大价值:" << maxVal << endl; // 输出10(选5个物品1,价值2*5=10)
    return 0;
}

新手:就改了遍历顺序?也太简单了吧!那多重背包呢?

四、进阶变种:多重背包(每个物品有选的次数限制)

新手 :多重背包是"每个物品最多选k[i]次",这个该怎么处理?
导师:多重背包的核心是"把有限次数的物品,转化为01背包的形式",常用两种方法:

  1. 朴素拆分:把第i个物品拆成k[i]个独立物品,直接用01背包(简单但效率低);
  2. 二进制拆分:把k[i]拆成2的幂次(1,2,4,...),减少拆分后的物品数量(高效,实战常用)。

(一)朴素拆分法(理解用)

比如物品i最多选3次,就拆成3个相同的物品,然后用01背包求解。

代码实现(C++)
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 多重背包:朴素拆分法(转化为01背包)
int multiplePackNaive(int n, int C, vector<int>& w, vector<int>& v, vector<int>& k) {
    // 拆分物品:把每个物品拆成k[i]个
    vector<int> new_w, new_v;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= k[i]; ++j) {
            new_w.push_back(w[i]);
            new_v.push_back(v[i]);
        }
    }
    // 用01背包求解
    vector<int> dp(C + 1, 0);
    int m = new_w.size();
    for (int i = 0; i < m; ++i) {
        for (int j = C; j >= new_w[i]; --j) {
            dp[j] = max(dp[j], dp[j - new_w[i]] + new_v[i]);
        }
    }
    return dp[C];
}

int main() {
    // 测试案例:n=2个物品,背包容量C=5
    // 物品1:w=1, v=2, k=3(最多选3次);物品2:w=2, v=3, k=1(最多选1次)
    int n = 2, C = 5;
    vector<int> w = {0, 1, 2};
    vector<int> v = {0, 2, 3};
    vector<int> k = {0, 3, 1};
    
    int maxVal = multiplePackNaive(n, C, w, v, k);
    cout << "朴素版多重背包最大价值:" << maxVal << endl; // 输出8(3个物品1+1个物品2:3*2+3=9?不对,3*1+2=5(容量),3*2+3=9)
    return 0;
}

新手:朴素法效率太低了,如果k[i]是1e5,拆分后物品数量会爆炸!二进制拆分法怎么弄?

(二)二进制拆分法(实战常用)

导师:二进制拆分的核心是:任何整数k都可以拆成若干个2的幂次之和(比如7=1+2+4,10=1+2+4+3),这样拆分后物品数量从k个降到log2(k)个,效率大幅提升。

比如k[i]=5,拆成1+2+2(1+2=3,剩下2),每个拆分后的"组合物品"代表选1次、2次、2次,这样就能覆盖0~5次的所有选择。

代码实现(C++)
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 多重背包:二进制拆分法(高效版)
int multiplePackBinary(int n, int C, vector<int>& w, vector<int>& v, vector<int>& k) {
    vector<int> new_w, new_v;
    for (int i = 1; i <= n; ++i) {
        int cnt = k[i]; // 第i个物品最多选cnt次
        // 二进制拆分:1,2,4,...
        for (int j = 1; j <= cnt; j *= 2) {
            new_w.push_back(w[i] * j);
            new_v.push_back(v[i] * j);
            cnt -= j;
        }
        // 处理剩余的数量
        if (cnt > 0) {
            new_w.push_back(w[i] * cnt);
            new_v.push_back(v[i] * cnt);
        }
    }
    // 01背包求解
    vector<int> dp(C + 1, 0);
    int m = new_w.size();
    for (int i = 0; i < m; ++i) {
        for (int j = C; j >= new_w[i]; --j) {
            dp[j] = max(dp[j], dp[j - new_w[i]] + new_v[i]);
        }
    }
    return dp[C];
}

int main() {
    int n = 2, C = 5;
    vector<int> w = {0, 1, 2};
    vector<int> v = {0, 2, 3};
    vector<int> k = {0, 3, 1};
    
    int maxVal = multiplePackBinary(n, C, w, v, k);
    cout << "二进制拆分版多重背包最大价值:" << maxVal << endl; // 输出9
    return 0;
}

新手:二进制拆分太巧妙了!除了这三种,还有其他背包变种吗?

五、其他常见背包变种(简要解析)

导师:还有两种高频变种,咱们快速过一下核心思路:

(一)混合背包(01+完全+多重)

核心:遍历每个物品时,先判断物品类型(01/完全/多重),再用对应方法处理:

  • 01背包:容量从后往前;
  • 完全背包:容量从前往后;
  • 多重背包:二进制拆分后按01背包处理。

(二)分组背包(物品分若干组,每组最多选1个)

核心 :状态定义dp[j]为容量j的最大价值,遍历顺序:

  1. 遍历每组;
  2. 容量从后往前遍历;
  3. 遍历组内每个物品,更新dp[j] = max(dp[j], dp[j - w[i]] + v[i])
代码片段(C++)
cpp 复制代码
// 分组背包核心代码
for (int i = 1; i <= group_num; ++i) { // 遍历每组
    for (int j = C; j >= 0; --j) { // 容量从后往前
        for (auto& item : group[i]) { // 遍历组内物品
            if (j >= item.w) {
                dp[j] = max(dp[j], dp[j - item.w] + item.v);
            }
        }
    }
}

六、背包DP的常见坑点与优化

新手 :学习过程中容易踩哪些坑?有没有通用优化技巧?
导师:新手最容易踩的4个坑:

  1. 01背包遍历顺序:必须从后往前,否则变成完全背包;
  2. 完全背包遍历顺序:必须从前往后,否则和01背包一样;
  3. 物品下标:建议从1开始(避免处理0的边界);
  4. 二进制拆分:剩余数量要单独处理(比如k=5,拆1+2+2,不是1+2)。

通用优化技巧:

  1. 空间优化:所有背包都能优化到一维数组(核心是滚动数组);
  2. 剪枝:如果物品重量超过背包容量,直接跳过;
  3. 价值优化:如果物品价值为0,直接跳过(无意义)。

七、总结

核心要点回顾

  1. 背包DP核心 :状态定义(dp[j]表示容量j的最大价值)+ 状态转移(选/不选物品的最大值);
  2. 01背包:每个物品选1次,容量从后往前遍历,一维空间O©;
  3. 完全背包:每个物品选无限次,容量从前往后遍历;
  4. 多重背包:每个物品选k次,二进制拆分后按01背包处理(高效);
  5. 遍历顺序:01/分组背包从后往前,完全背包从前往后。

学习建议

背包DP是DP的入门基石,建议你:

  1. 先吃透01背包(二维→一维),这是所有变种的基础;
  2. 手动推导小案例的dp数组(比如n=3、C=5),理解状态转移的过程;
  3. 从简单变种(完全背包)到复杂变种(分组背包)逐步练习;
  4. 结合真题(比如NOIP、蓝桥杯的背包题)巩固,把模板转化为自己的思路。

记住:DP的核心不是背模板,而是理解"状态定义"和"状态转移"------只要这两个点想清楚,任何背包变种都能迎刃而解。

相关推荐
尘缘浮梦1 小时前
协程asyncio入门案例 2
开发语言·python
juleskk1 小时前
2.15 复试训练
开发语言·c++·算法
一个处女座的程序猿O(∩_∩)O2 小时前
Python面向对象的多态特性详解
开发语言·python
yngsqq2 小时前
多段线顶点遍历技巧(适用闭合和非闭合)
开发语言
宇木灵2 小时前
C语言基础-五、数组
c语言·开发语言·学习·算法
楼田莉子2 小时前
Linux学习:线程的同步与互斥
linux·运维·c++·学习
xyq20242 小时前
空对象模式
开发语言
liulun3 小时前
C++ WinRT中的异步
c++·windows