定义

背包问题(Knapsack Problem)是动态规划中最经典的入门问题之一。给定 件物品,每件物品 有重量 和价值 ,以及一个容量为 的背包。求解如何选择物品装入背包,在不超过背包容量的前提下,使背包中物品的总价值最大。1

核心概念

状态定义

表示考虑前 件物品,背包容量为 时能获得的最大价值。答案为

状态转移方程:

其中:

  • 表示不选第 件物品
  • 表示选第 件物品(前提是

空间优化

观察到状态转移只与 有关,可以将二维数组压缩为一维数组。但需要注意遍历顺序:

0-1 背包:容量需要逆序遍历,保证每个物品只被使用一次。

完全背包:容量需要正序遍历,允许物品重复使用。

时间复杂度为 ,空间复杂度为


0-1 背包

问题描述

每件物品只能选择一次(选或不选),求解最大价值。

代码实现

#include <bits/stdc++.h>
using namespace std;
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, W;
    cin >> n >> W;
    vector<int> w(n+1), v(n+1);
    for (int i = 1; i <= n; i++) cin >> w[i] >> v[i];
    vector<int> f(W+1, 0);
    for (int i = 1; i <= n; i++)
        for (int j = W; j >= w[i]; j--)
            f[j] = max(f[j], f[j-w[i]] + v[i]);
    cout << f[W] << endl;
    return 0;
}

为什么逆序遍历?

假设物品重量 ,价值 ,背包容量

如果正序遍历(错误):

  • 时:
  • 时: 不变
  • 时:

此时 使用了两次物品,违反了 0-1 背包的限制。

逆序遍历则保证先更新大的容量,再更新小的容量, 时仍是上一轮的值:

  • 时:
  • 时:旧值
  • 时:
  • 时:

模板

vector<int> f(W+1, 0);
for (int i = 1; i <= n; i++)
    for (int j = W; j >= w[i]; j--)
        f[j] = max(f[j], f[j-w[i]] + v[i]);

完全背包

问题描述

每件物品可以选择无限次,求解最大价值。

与 0-1 背包的区别

完全背包的状态转移方程为:

与 0-1 背包的区别在于:正序遍历,使得在更新 时, 已经是本轮更新后的值(即考虑了当前物品多次使用)。

代码实现

#include <bits/stdc++.h>
using namespace std;
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, W;
    cin >> n >> W;
    vector<int> w(n+1), v(n+1);
    for (int i = 1; i <= n; i++) cin >> w[i] >> v[i];
    vector<int> f(W+1, 0);
    // 完全背包 - 正序遍历
    for (int i = 1; i <= n; i++)
        for (int j = w[i]; j <= W; j++)
            f[j] = max(f[j], f[j-w[i]] + v[i]);
    cout << f[W] << endl;
    return 0;
}

模板

// Complete knapsack - items can be taken unlimited times
// Loop direction is FORWARD (opposite of 0-1 knapsack)
for (int i = 1; i <= n; i++)
    for (int j = w[i]; j <= W; j++)
        f[j] = max(f[j], f[j-w[i]] + v[i]);

对比总结

类型遍历方向含义
0-1 背包逆序每个物品只用一次
完全背包正序每个物品可用无限次

多重背包

问题描述

件物品最多只能选 次,求解最大价值。

朴素解法

for (int i = 1; i <= n; i++)
    for (int j = W; j >= w[i]; j--)
        for (int t = 1; t <= k[i] && j >= t * w[i]; t++)
            f[j] = max(f[j], f[j-t*w[i]] + t*v[i]);

时间复杂度为 ,当 较大时效率低下。

二进制分组优化

将物品数量 拆分成若干个”二进制分组”:

每个分组视为一个 0-1 背包的物品。例如 ,可以拆分为 四个分组,总组合数仍然可以表示 的任意数量。

优化后时间复杂度降为

代码实现

#include <bits/stdc++.h>
using namespace std;
 
struct Item {
    int w, v;
};
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int m, W;
    cin >> m >> W;  // m: 物品种类数,W: 背包容量
    vector<Item> list;  // 拆分后的物品列表
    list.reserve(1000);
    
    for (int i = 1; i <= m; i++) {
        int p, h, k;
        cin >> p >> h >> k;  // p: 重量,h: 价值,k: 数量
        int c = 1;
        while (k - c > 0) {
            k -= c;
            list.push_back({c * p, c * h});
            c *= 2;
        }
        // 剩余部分
        list.push_back({p * k, h * k});
    }
    
    // 转换为 0-1 背包
    vector<int> f(W+1, 0);
    for (auto& item : list) {
        for (int j = W; j >= item.w; j--)
            f[j] = max(f[j], f[j-item.w] + item.v);
    }
    
    cout << f[W] << endl;
    return 0;
}

二进制分组模板

// Binary grouping optimization for multi-knapsack
int index = 0;
for (int i = 1; i <= m; i++) {
    int c = 1, p, h, k;
    cin >> p >> h >> k;  // price, value, count
    while (k - c > 0) {
        k -= c;
        list[++index].w = c * p;
        list[index].v = c * h;
        c *= 2;
    }
    list[++index].w = p * k;
    list[index].v = h * k;
}

三种背包对比

类型物品限制遍历顺序时间复杂度
0-1 背包选 0 或 1 次逆序
完全背包无限次正序
多重背包有限次 逆序 + 二进制优化

典型应用

1. 恰好装满 vs 可以不装满

上述代码默认可以不装满)。

若要求恰好装满,需初始化为负无穷:

vector<int> f(W+1, -INF);
f[0] = 0;

2. 求方案数

换成求和:

vector<int> f(W+1, 0);
f[0] = 1;
for (int i = 1; i <= n; i++)
    for (int j = W; j >= w[i]; j--)
        f[j] += f[j-w[i]];

3. 依赖背包

物品之间存在依赖关系(如选了物品 才能选物品 ),可以将依赖关系转化为”01 背包 + 分组背包”的组合形式。


总结

背包问题是动态规划的基石。核心在于:

  1. 状态定义 表示前 件物品、容量 下的最优价值
  2. 状态转移 决策,选或不选
  3. 空间优化:一维数组 + 遍历顺序控制
  4. 多重背包:二进制分组转化为 0-1 背包

掌握这三种背包问题后,可以进一步学习单调栈等更复杂的 DP 优化技巧。


Footnotes

  1. 背包问题九讲 - 崔添翼 https://github.com/tianyicui/pack