背包DP之混合背包
背包DP之混合背包
- 一、混合背包基础认知
- 1.1 问题定义
- 1.2 核心特征
- 二、求解框架:分类处理+统一DP
- 2.1 核心思路
- 2.2 状态定义与递推
- 三、代码实现
- 3.1 完整实现
- 3.2 代码解析
- 四、实例推演
- 4.1 输入物品
- 4.2 DP更新过程
- 五、混合背包的变种与优化
- 5.1 变种问题
- 5.2 优化技巧
- 三类背包物品处理逻辑对比
- 总结
实际的背包问题中,物品往往不会是单一类型——可能既有“只能选一次”的0/1背包物品,又有“可无限选”的完全背包物品,还有“最多选k次”的多重背包物品,这种“混合背包”模型更贴近现实场景(如“限量商品+常规商品+限购商品”的组合),但求解时需要整合多种背包的处理逻辑。
一、混合背包基础认知
1.1 问题定义
给定n
种物品,每种物品属于以下三类之一:
- 0/1背包物品:最多选择1次(如限量纪念品);
- 完全背包物品:可无限选择(如常规商品);
- 多重背包物品:最多选择
cnt[i]
次(如限购3件的促销商品)。
每种物品有重量w[i]
和价值v[i]
,背包容量为C
。求在总重量不超过C
的前提下,能装入物品的最大总价值。
示例:
- 输入:
n=3, C=10
- 物品0(0/1):
w=3, v=5
(最多1次) - 物品1(完全):
w=2, v=3
(无限次) - 物品2(多重):
w=4, v=6, cnt=2
(最多2次)
- 物品0(0/1):
- 输出:
17
(物品0×1 + 物品1×1 + 物品2×2 → 重量3+2+8=13>10;正确最优:物品1×3(2×3=6重量,3×3=9) + 物品2×1(4重量,6) → 总重量10,价值15;或物品2×2(8重量,12) + 物品1×1(2重量,3) → 总重量10,价值15?实际最优为物品0(3,5)+物品2×2(8,12)→ 重量11>10,因此正确输出15)
1.2 核心特征
- 多类型共存:物品分三类,需针对每种类型采用对应处理逻辑。
- 统一约束:所有物品共享背包容量
C
,总重量不得超过C
。 - 求解难点:如何在同一框架中整合0/1、完全、多重背包的处理逻辑,避免冲突。
混合背包的本质是“多类型物品的容量分配优化”,核心思路是“分类处理,统一更新”——将所有物品转化为可按统一规则处理的形式,再用背包DP求解。
二、求解框架:分类处理+统一DP
2.1 核心思路
混合背包的求解可分为两步:
-
标准化处理:将三类物品转化为“可按统一规则处理的形式”:
- 0/1背包物品:直接保留(本身符合“最多1次”规则)。
- 完全背包物品:保留(需按“无限次”规则处理)。
- 多重背包物品:通过“二进制拆分”转化为0/1背包物品(拆分后符合“最多1次”规则)。
-
统一DP更新:用一维DP数组,根据物品类型选择遍历顺序(0/1和拆分后的多重物品用逆序,完全物品用正序),累计更新最大价值。
2.2 状态定义与递推
沿用一维DP数组:
dp[j]
表示“容量为j
时的最大价值”。- 递推逻辑根据物品类型调整:
- 0/1或拆分后的多重物品:逆序遍历容量
j
,dp[j] = max(dp[j], dp[j - w] + v)
(避免重复选择)。 - 完全背包物品:正序遍历容量
j
,dp[j] = max(dp[j], dp[j - w] + v)
(允许重复选择)。
- 0/1或拆分后的多重物品:逆序遍历容量
三、代码实现
3.1 完整实现
import java.util.*;public class HybridKnapsack {// 物品类型枚举enum ItemType {ZERO_ONE, // 0/1背包UNBOUNDED, // 完全背包MULTIPLE // 多重背包}// 物品类static class Item {int w; // 重量int v; // 价值ItemType type; // 类型int cnt; // 数量(仅多重背包用)public Item(int w, int v, ItemType type) {this(w, v, type, 0);}public Item(int w, int v, ItemType type, int cnt) {this.w = w;this.v = v;this.type = type;this.cnt = cnt;}}/*** 混合背包求解* @param items 物品列表(含三类物品)* @param C 背包容量* @return 最大价值*/public int maxValue(List<Item> items, int C) {int[] dp = new int[C + 1];for (Item item : items) {int w = item.w;int v = item.v;switch (item.type) {case ZERO_ONE:// 0/1背包:逆序遍历for (int j = C; j >= w; j--) {dp[j] = Math.max(dp[j], dp[j - w] + v);}break;case UNBOUNDED:// 完全背包:正序遍历for (int j = w; j <= C; j++) {dp[j] = Math.max(dp[j], dp[j - w] + v);}break;case MULTIPLE:// 多重背包:二进制拆分后按0/1处理List<Item> splitItems = splitMultipleItem(item);for (Item split : splitItems) {int sw = split.w;int sv = split.v;for (int j = C; j >= sw; j--) {dp[j] = Math.max(dp[j], dp[j - sw] + sv);}}break;}}return dp[C];}// 二进制拆分多重物品private List<Item> splitMultipleItem(Item multipleItem) {List<Item> splitItems = new ArrayList<>();int w = multipleItem.w;int v = multipleItem.v;int cnt = multipleItem.cnt;for (int k = 1; k <= cnt; k *= 2) {splitItems.add(new Item(k * w, k * v, ItemType.ZERO_ONE));cnt -= k;}if (cnt > 0) {splitItems.add(new Item(cnt * w, cnt * v, ItemType.ZERO_ONE));}return splitItems;}public static void main(String[] args) {HybridKnapsack solver = new HybridKnapsack();List<Item> items = new ArrayList<>();// 物品0:0/1背包(w=3, v=5)items.add(new Item(3, 5, ItemType.ZERO_ONE));// 物品1:完全背包(w=2, v=3)items.add(new Item(2, 3, ItemType.UNBOUNDED));// 物品2:多重背包(w=4, v=6, cnt=2)items.add(new Item(4, 6, ItemType.MULTIPLE, 2));int C = 10;System.out.println(solver.maxValue(items, C)); // 输出15}
}
3.2 代码解析
- 物品分类:用
ItemType
枚举区分三类物品,Item
类存储重量、价值及类型(多重物品额外存储数量)。 - 分类处理:
- 0/1物品:逆序遍历容量,避免重复选择。
- 完全物品:正序遍历容量,允许无限选择。
- 多重物品:先通过
splitMultipleItem
二进制拆分(转化为0/1物品),再逆序遍历。
- 统一更新:所有物品最终通过一维DP数组更新,累计最大价值。
四、实例推演
4.1 输入物品
- 物品0(0/1):
w=3, v=5
- 物品1(完全):
w=2, v=3
- 物品2(多重,cnt=2):
w=4, v=6
→ 拆分后为(4,6)
和(4,6)
(0/1物品)。
4.2 DP更新过程
-
初始状态:
dp = [0,0,0,0,0,0,0,0,0,0,0]
(容量0~10)。 -
处理物品0(0/1):
- 逆序遍历
j=10→3
:j=3
:dp[3] = max(0, dp[0]+5) =5
j=4~10
:dp[j] = max(0, dp[j-3]+5)
(如j=5
→dp[2]+5=5
)
- 处理后:
dp = [0,0,0,5,5,5,5,5,5,5,5]
。
- 逆序遍历
-
处理物品1(完全):
- 正序遍历
j=2→10
:j=2
:dp[2] = max(0, dp[0]+3)=3
j=4
:dp[4] = max(5, dp[2]+3)=6
j=6
:dp[6] = max(5, dp[4]+3)=9
j=8
:dp[8] = max(5, dp[6]+3)=12
j=10
:dp[10] = max(5, dp[8]+3)=15
- 处理后:
dp = [0,0,3,5,6,8,9,11,12,14,15]
。
- 正序遍历
-
处理物品2(拆分后两个0/1物品):
- 第一个拆分物品
(4,6)
:- 逆序遍历
j=10→4
:j=10
:dp[10] = max(15, dp[6]+6)=9+6=15
(不变)j=8
:dp[8] = max(12, dp[4]+6)=6+6=12
(不变)
- 逆序遍历
- 第二个拆分物品
(4,6)
:- 逆序遍历
j=10→4
:j=10
:dp[10] = max(15, dp[6]+6)=15
(不变)j=8
:dp[8] = max(12, dp[4]+6)=12
(不变)
- 逆序遍历
- 最终
dp[10] =15
。
- 第一个拆分物品
五、混合背包的变种与优化
5.1 变种问题
-
二维约束混合背包:
- 问题:物品有重量和体积两个约束,需同时满足。
- 解法:用二维DP数组
dp[j][k]
(j
重量,k
体积),按类型选择遍历顺序(0/1和多重逆序,完全正序)。
-
带依赖的混合背包:
- 问题:部分物品有依赖(如选B必须先选A),且依赖物品可能分属不同类型。
- 解法:先处理依赖关系(如树形结构),再按类型处理每个节点的物品。
-
最大化数量而非价值:
- 问题:目标是最大化物品数量(而非价值),有价值上限约束。
- 解法:交换价值和重量的角色(重量设为价值,价值设为1),按原框架求解。
5.2 优化技巧
-
预处理剪枝:
- 移除“无效物品”:若物品A(w1,v1)和物品B(w2,v2)同类型,且
w1≥w2
且v1≤v2
,则A无效(选B更优)。 - 容量上限优化:对完全背包物品,若
w > C
,可直接忽略(无法装入)。
- 移除“无效物品”:若物品A(w1,v1)和物品B(w2,v2)同类型,且
-
拆分优化:
- 多重物品拆分时,若
w×cnt ≤ C
,可按完全背包处理(减少拆分次数)。 - 例:物品
w=2, cnt=5, C=10
→2×5=10 ≤10
,可视为完全背包(反正最多选5次,正序遍历自然不会超过)。
- 多重物品拆分时,若
-
遍历顺序调整:
- 先处理0/1和多重物品(逆序),再处理完全物品(正序),避免完全物品的正序更新干扰前两类物品的状态。
三类背包物品处理逻辑对比
物品类型 | 核心特征 | 处理逻辑 | 遍历顺序 |
---|---|---|---|
0/1背包 | 最多选1次 | 直接按0/1规则更新 | 逆序(C→w ) |
完全背包 | 可无限选 | 按完全规则更新 | 正序(w→C ) |
多重背包 | 最多选cnt 次 | 二进制拆分后按0/1规则更新 | 逆序(C→w ) |
总结
混合背包的核心是“分类处理,统一整合”——通过将不同类型的物品转化为可按对应规则处理的形式,在同一DP框架中累计更新最大价值。注意点如下:
- 类型识别:准确区分物品类型,选择对应处理逻辑(0/1逆序、完全正序、多重拆分)。
- 拆分技巧:多重物品通过二进制拆分转化为0/1物品,平衡效率与复杂度。
- 统一更新:用一维DP数组作为载体,按类型调整遍历顺序,避免状态冲突。
That’s all, thanks for reading~~
觉得有用就点个赞
、收进收藏
夹吧!关注
我,获取更多干货~