背包问题变体:状态设计的通用框架与工程映射
2026/6/11 16:13:08 网站建设 项目流程

背包问题变体:状态设计的通用框架与工程映射

一、从一道题到一类题:背包问题的状态抽象困境

背包问题是动态规划中变体最多的题型。01 背包、完全背包、多重背包、分组背包、二维费用背包——每种变体都有不同的约束条件,但初学者往往为每种变体单独记忆状态转移方程,遇到混合变体(如"分组 + 二维费用")就无从下手。根本原因在于没有建立统一的状态设计框架:背包问题的核心是"在容量约束下选择物品使价值最大化",状态设计的差异仅来自约束维度的增减。

某算法平台统计:在 2000 道 DP 题目中,约 15% 可归入背包模型,但能正确识别背包模型并完成状态设计的选手不到 40%。问题不在于转移方程的推导,而在于如何将题目描述映射到正确的约束维度。

二、背包问题的统一状态框架

所有背包变体都可以用统一的状态框架描述:dp[i][c1][c2]...[ck]表示从前 i 个物品中选择,在约束 c1, c2, ..., ck 下的最大价值。约束维度决定了状态空间的大小和转移方向。

flowchart TD A[背包问题] --> B{物品选择次数} B -->|每个最多选1次| C[01背包] B -->|每个可选无限次| D[完全背包] B -->|每个可选s_i次| E[多重背包] A --> F{额外约束} F -->|两组互斥| G[分组背包] F -->|两种容量| H[二维费用背包] F -->|必须恰好装满| I[恰好装满变体] C --> J[状态: dp[i][c]] D --> K[状态: dp[i][c], 转移方向不同] E --> L[二进制拆分 → 01背包] G --> M[状态: dp[i][c], 组内取max] H --> N[状态: dp[i][c1][c2]]

2.1 01 背包:基础状态与空间优化

状态定义:dp[i][c]= 从前 i 个物品中选,容量不超过 c 的最大价值。

转移方程:

  • 不选物品 i:dp[i][c] = dp[i-1][c]
  • 选物品 i:dp[i][c] = dp[i-1][c-w_i] + v_i(前提 c ≥ w_i)

空间优化:逆序遍历容量,一维数组即可。逆序的原因是保证dp[c-w_i]引用的是上一层(i-1)的值,而非本层已更新的值。

2.2 完全背包:转移方向的改变

完全背包中每个物品可选无限次,转移方程变为:

  • dp[i][c] = max(dp[i-1][c], dp[i][c-w_i] + v_i)

注意第二项是dp[i][c-w_i]而非dp[i-1][c-w_i],表示"在同一层内"继续选择物品 i。空间优化时正序遍历容量即可。

2.3 多重背包:二进制拆分

每个物品有数量上限 s_i,朴素做法将每个物品拆成 s_i 个 01 背包物品,时间复杂度 O(n × C × Σs_i)。二进制拆分将 s_i 分解为 1, 2, 4, ..., 2^k, remainder,物品数降为 O(log s_i),总复杂度 O(n × C × log(max_s))。

三、生产级代码实现

3.1 通用背包框架

from typing import List, Tuple def knapsack_01(items: List[Tuple[int, int]], capacity: int) -> int: """ 01背包:每个物品最多选一次。 items: [(weight, value), ...] 逆序遍历容量,保证每个物品只被选一次。 """ dp = [0] * (capacity + 1) for w, v in items: for c in range(capacity, w - 1, -1): # 逆序 dp[c] = max(dp[c], dp[c - w] + v) return dp[capacity] def knapsack_complete(items: List[Tuple[int, int]], capacity: int) -> int: """ 完全背包:每个物品可选无限次。 正序遍历容量,允许同一物品被多次选择。 """ dp = [0] * (capacity + 1) for w, v in items: for c in range(w, capacity + 1): # 正序 dp[c] = max(dp[c], dp[c - w] + v) return dp[capacity] def knapsack_multiple( items: List[Tuple[int, int, int]], capacity: int ) -> int: """ 多重背包:每个物品有数量上限。 items: [(weight, value, count), ...] 二进制拆分将 O(n*C*Σs) 优化为 O(n*C*log(max_s)) """ dp = [0] * (capacity + 1) for w, v, s in items: # 二进制拆分 k = 1 while s > 0: take = min(k, s) # 拆分后的物品按 01 背包处理 item_w, item_v = w * take, v * take for c in range(capacity, item_w - 1, -1): dp[c] = max(dp[c], dp[c - item_w] + item_v) s -= take k *= 2 return dp[capacity]

3.2 分组背包

def knapsack_grouped( groups: List[List[Tuple[int, int]]], capacity: int ) -> int: """ 分组背包:每组最多选一个物品。 groups: [[(w1,v1), (w2,v2), ...], [...], ...] 外层遍历组,内层逆序遍历容量,组内遍历物品取 max。 """ dp = [0] * (capacity + 1) for group in groups: # 逆序遍历容量,保证每组只选一个 for c in range(capacity, 0, -1): for w, v in group: if c >= w: dp[c] = max(dp[c], dp[c - w] + v) return dp[capacity]

3.3 二维费用背包

def knapsack_2d( items: List[Tuple[int, int, int]], cap1: int, cap2: int ) -> int: """ 二维费用背包:两种容量约束(如重量和体积)。 items: [(w1, w2, value), ...] 状态从一维扩展到二维,两层逆序遍历。 """ dp = [[0] * (cap2 + 1) for _ in range(cap1 + 1)] for w1, w2, v in items: for c1 in range(cap1, w1 - 1, -1): for c2 in range(cap2, w2 - 1, -1): dp[c1][c2] = max(dp[c1][c2], dp[c1 - w1][c2 - w2] + v) return dp[cap1][cap2]

四、状态设计的边界与权衡

  1. 状态空间爆炸:二维费用背包的状态空间为 O(C1 × C2),当两种容量都达到 10⁵ 时,状态数达到 10¹⁰,内存和计算均不可行。解决方案是将一种约束从状态维度降为物品属性(如限制物品总数),或使用贪心近似。

  2. 恰好装满变体:初始化方式不同——dp[0] = 0,其余dp[c] = -∞。这保证了只有"恰好用完容量"的方案才被计入。工程中对应"预算必须花完"的资源分配问题。

  3. 输出方案 vs 最优值:上述代码只输出最优值。若需还原选择方案,需要额外记录转移路径(choice数组),空间开销增加 O(n × C)。对于只需最优值的场景,不应保留转移路径。

  4. 浮点数容量:当容量为浮点数时(如概率约束),需要乘以精度因子转为整数,或改用基于价值的 DP(最小化容量使价值达到目标)。精度转换可能引入舍入误差,需要验证误差在可接受范围内。

  5. 大规模数据的局限:背包问题的伪多项式复杂度 O(n × C) 在 C 达到 10⁹ 时不可行。此时应考虑贪心近似(FPTAS)或分支限界法,牺牲最优性换取可行性。

五、总结

背包问题的核心是约束维度驱动的状态设计:01 背包是基础,完全背包改变遍历方向,多重背包用二进制拆分降维,分组背包增加组内互斥约束,二维费用背包扩展约束维度。工程中,资源分配、投资组合、任务调度等问题都可以映射到背包模型。关键不是记忆每种变体的方程,而是掌握"约束维度 → 状态空间 → 转移方向"的推导链路,从而在面对混合变体时快速构建正确的状态模型。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询