0/1 背包的状态转移方程如何建立?滚动数组如何优化空间?

十年
2025-08-23 / 0 评论 / 0 阅读 / 正在检测是否收录...
加载耗时 17ms
温馨提示:
本文最后更新于2025年08月24日,已超过387天没有更新,若内容或图片失效,请留言反馈。
转载说明:本文整理转载自开源项目 PHP-Interview-QA(Apache-2.0 License,作者 @colinlet),感谢开源分享,版权归原作者所有。

一句话结论

定义 dp[i][w] 为前 i 件物品放入容量 w 背包时的最大价值,逆序遍历可压缩到一维 O(W) 空间。

体系讲解

建模三步骤

DP 建模的通用流程是:定状态 → 写转移 → 定边界,以 0/1 背包为示范:

  1. 定状态dp[i][w] = 从前 i 件物品中任意选取、总重量不超过 w 时的最大价值
  2. 写转移:第 i 件物品重量 wi、价值 vi,对每个容量 w 有且仅有两个决策:

    • 不放第 i 件:dp[i][w] = dp[i-1][w]
    • 放第 i 件(要求 w ≥ wi):dp[i][w] = dp[i-1][w-wi] + vi
    • 合并:dp[i][w] = max(dp[i-1][w], dp[i-1][w-wi] + vi)
  3. 定边界dp[0][*] = 0(无物品可选),dp[*][0] = 0(容量为零)
flowchart LR
    A["dp[i-1][w]\n不放第 i 件"] --> C["dp[i][w] = max(A, B)"]
    B["dp[i-1][w-wi] + vi\n放第 i 件"] --> C

空间优化:滚动数组

二维状态 dp[i][w] 的第 i 行只依赖第 i-1 行,可原地压缩为一维 dp[w]

遍历方向效果适用
逆序(W → wi)dp[w-wi] 使用的仍是「上一行」值,每件只选一次0/1 背包
正序(wi → W)dp[w-wi] 已被本轮更新,相当于允许重复选取完全背包

逆序遍历是 0/1 背包压缩的核心约束,本质是保持"当前物品还未被选入当前行"的不变量。

复杂度对比

版本时间空间
二维 DPO(n·W)O(n·W)
滚动数组O(n·W)O(W)

考察意图

考察候选人能否将组合优化问题抽象为 DP 状态空间,以及是否理解滚动数组背后的数据依赖原理。这是所有背包变体(完全背包、多重背包)的基础,也是"能否从原理推导出代码"的典型分水岭。

追问链

  1. 完全背包与 0/1 背包在代码上如何区分?
    压缩后改为正向遍历(wwiW),使 dp[w-wi] 被当前轮更新后可再次使用,等价于允许同一物品无限选取。
  2. 若要求恰好装满背包,初始化应如何调整?
    dp[0] = 0,其余初始化为 PHP_INT_MIN(负无穷语义);转移时跳过负无穷来源;若最终 dp[W] < 0 则无方案。
  3. 多重背包(每件最多 k 次)如何处理?
    朴素做法将每件拆成 k 个 0/1 物品,O(n·k·W);二进制拆分(拆成 1、2、4…件)可降至 O(n·log k·W);单调队列优化可达 O(n·W)。
  4. W 极大(如 10⁹)时数组无法分配,如何应对?
    若物品数 n 极小,可转换维度:dp[v] = 达到总价值 v 所需最小重量,遍历价值而非容量,时间 O(n·V),V 为价值总和上限。

易错点

  1. 逆序 vs 正序混淆:一维压缩时若写成正序,同一物品会被多次选入,悄悄变成完全背包语义,不报错只是结果偏大。
  2. "恰好装满"与"至多装满"初始化混淆:前者非零格须初始化为 -∞,后者全 0 即可;两者题干微小差异极易导致初始化方式写错。
  3. 循环上界漏等号:内层循环应为 $w >= $weights[$i](含等号),写成 > 会跳过恰好等于物品重量的容量格。

代码示例

<?php

/**
 * 0/1 背包(一维滚动数组)
 *
 * @param int[] $weights 物品重量数组
 * @param int[] $values  物品价值数组
 * @param int   $W       背包最大容量
 * @return int 可获得的最大价值
 */
function knapsack01(array $weights, array $values, int $W): int
{
    $n  = count($weights);
    $dp = array_fill(0, $W + 1, 0); // dp[w] = 容量 w 时的最大价值

    for ($i = 0; $i < $n; $i++) {
        // 逆序遍历:保证 dp[w - weights[i]] 仍是放入第 i 件之前的状态
        for ($w = $W; $w >= $weights[$i]; $w--) {
            $dp[$w] = max($dp[$w], $dp[$w - $weights[$i]] + $values[$i]);
        }
    }

    return $dp[$W];
}

// 示例:3 件物品,背包容量 5
// 物品0:重2价3,物品1:重3价4,物品2:重4价5
$weights = [2, 3, 4];
$values  = [3, 4, 5];

echo knapsack01($weights, $values, 5); // 输出 7(选物品0+物品1:重量5,价值7)
本文共 809 个字数,平均阅读时长 ≈ 3分钟
0

评论

博主关闭了当前页面的评论