跳到主要内容

0-1 背包

0-1 背包问题是什么?​

  • 条件: n 件物品, 背包重量上限 cap; 第 i 件物品重量 wgt[i], 价值 val[i];
  • 限制: 每件物品最多选一次;
  • 目标: 求装入物品的最大总价值;
  • 阶段: 按物品序号推进, 每阶段把重量维度填满;

状态与转移方程如何写?​

  • 状态: dp[i][c] 为前 i 件物品放入容量 c 的背包能获得的最大价值;
  • 决策: 只考虑第 i 件物品, 选或不选;
    • 不选: dp[i][c] = dp[i-1][c];
    • 选: 需 wgt[i-1] <= c, dp[i][c] = dp[i-1][c - wgt[i-1]] + val[i-1];
  • 方程: dp[i][c] = max(dp[i-1][c], dp[i-1][c - wgt[i-1]] + val[i-1]);
  • 初值: 没有物品或容量为 0 时价值为 0, 即 dp[0][*] = dp[*][0] = 0;
  • 答案: dp[n][cap];
  • 复杂度: 时间 O(n * cap), 空间 O(n * cap);
const knapsackDP = (wgt, val, cap) => {
const n = wgt.length;
const dp = Array.from({ length: n + 1 }, () => new Array(cap + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let c = 0; c <= cap; c++) {
if (wgt[i - 1] > c) dp[i][c] = dp[i - 1][c];
else dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
return dp[n][cap];
};

滚动数组为什么必须倒序遍历?​

  • 依赖: 每个状态只和上一行 (dp[i-1]) 的同列与左列有关, 一行即可放下全部信息;
  • 一维写法: dp[c] = max(dp[c], dp[c - wgt[i-1]] + val[i-1]);
  • 倒序原因: dp[c - wgt] 必须是上一行的旧值; 正序遍历会被本轮刚写入的值覆盖, 等价于物品可重复选;
  • 复杂度: 时间 O(n * cap), 空间 O(cap);
const knapsackDPComp = (wgt, val, cap) => {
const dp = new Array(cap + 1).fill(0);
for (let i = 1; i <= wgt.length; i++) {
for (let c = cap; c >= wgt[i - 1]; c--) {
dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
return dp[cap];
};

背包问题有哪些常见变体?​

  • 按物品数量: 0-1 背包 (各一件)、完全背包 (无限件)、多重背包 (限量 count 件);
  • 按约束维度: 重量之外的附加约束 (体积、分组互斥、二维容量);
  • 按问题目标: 求最值、求组合数、求是否存在、求恰好装满;
  • 见 背包变种;