跳转至

背包 DP (Knapsack DP)

问题原型

给定容量为 \(m\) 的背包和 \(n\) 件物品(第 \(i\) 件物品重量为 \(w_i\),价值为 \(v_i\)),求不超过背包容量的最大价值。

核心思想

\(dp[i][j]\) 表示在容量为 \(j\) 的背包中,从前 \(i\) 件物品选择装入可达到的最大价值。

依次处理每个物品:

  • 若第 \(i\) 件物品不装入背包,则 \(dp[i][j] = dp[i−1][j]\)
  • 若第 \(i\) 件物品装入背包,则 \(dp[i][j] = dp[i−1][j−w_i] + v_i (j ≥ w_i)\)

通过状态转移方程实现从 \(dp[i-1][...]\)\(dp[i][...]\) 的递推更新,最终在 \(dp[n][m]\) 处得到最大价值。

1. 0/1 背包

特点:每件物品只能选或不选

状态转移方程

dp[i][j] = \max(dp[i - 1][j], dp[i - 1][j - w_i] + v_i)

实现

for (int i = 1; i <= n; i++) {
    for (int j = m; j >= w[i]; j--) {
        dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
    }
}

使用滚动数组简化

for (int i = 1; i <= n; i++) {
    for (int j = m; j >= w[i]; j--) { // 逆序遍历,防止一个物品被重复选取
        dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
    }
}

2. 完全背包

特点:每件物品可以选无限次

状态转移方程

dp[i][j] = \max(dp[i - 1][j], dp[i][j - w_i] + v_i)

实现

for (int i = 1; i <= n; i++) {
    for (int j = w[i]; j <= m; j++) { // 正序遍历,循环中可用到本轮已装入该物品的情况
        dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);
    }
}

使用滚动数组简化

for (int i = 1; i <= n; i++) {
    for (int j = w[i]; j <= m; j++) { // 正序遍历,循环中可用到本轮已装入该物品的情况
        dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
    }
}

3. 多重背包

特点:第 \(i\) 件物品最多选 \(s_i\)

思路:若直接循环枚举第 \(i\) 件物品 \(s_i\) 次,容易超时。于是用空间换时间,将 \(s_i\) 拆分为 \(2\) 的幂次,即 \(s_i = 2 ^ 0 + 2 ^ 1 + ... + 2 ^ {\lfloor \log_2(s_i + 1) \rfloor - 1} + o\) 。若 \(s_i + 1\)\(2\) 的整数次幂, \(o = 0\) ;否则 \(o = s_i - 2 ^ {\lfloor \log_2(s_i + 1) \rfloor - 1}\) 。以上拆分可通过组合表示任意 \(≤ s_i\) 个物品的等效选取方式,将问题转化为选/不选拆分后得到的新物品,随后做 0/1 背包即可。

实现

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= s[i]; j *= 2) { // 分解为2的幂次
        cnt++;
        w0[cnt] = w[i] * j;
        v0[cnt] = v[i] * j;
        s[i] -= j;
    }
    if (s[i] > 0) { // 处理剩余部分
        cnt++;
        w0[cnt] = w[i] * s[i];
        v0[cnt] = v[i] * s[i];
    }
}

// 0/1 背包
for (int i = 1; i <= cnt; i++) {
    for (int j = m; j >= w0[i]; j--) { // 逆序遍历,防止一个物品被重复选取
        dp[j] = max(dp[j], dp[j - w0[i]] + v0[i]);
    }
}

Notes

题目涉及的最多的模型还是 0/1 背包,即取 or 不取的问题。主要还是确定如何记录状态,以及列出状态转移方程。此外,更新状态时需注意正序/逆序遍历,防止非法的重复选取。

发现错误?想一起完善? 在 GitHub 上编辑此页

本文档内容作为个人算法笔记整理,代码模板可按需要参考和修改。