分组背包问题Python和C++两个版本讲解
分组背包问题详解(Python实现)
分组背包(Grouped Knapsack)是背包问题的一种变体,其核心特点是 物品被分成若干组,每组内的物品相互冲突,最多只能选择组内的一件物品。
问题描述:
- 背包容量:
V
- 物品组数:
G
- 每组物品: 第
g
组包含S_g
件物品。每组物品中的第i
件有:- 体积:
v[g][i]
- 价值:
w[g][i]
- 体积:
- 目标: 在不超过背包容量的前提下,从每组中至多选择一件物品,使得装入背包的物品总价值最大。
关键点:
- 分组结构: 物品天然或逻辑上被划分成组。
- 组内互斥: 同一组内的物品不能同时被选择。要么不选该组的任何物品,要么只选该组的一件物品。
解决方法(动态规划):
分组背包通常使用 动态规划 (Dynamic Programming) 解决。状态定义和状态转移方程如下:
-
状态定义:
dp[j]
:表示只考虑前g
组物品(组号从1开始计数),在背包容量恰好为j
时,所能获得的最大价值。- (也可以定义为容量不超过
j
的最大价值,状态转移逻辑类似)
-
状态转移方程:
对于当前正在考虑的第g
组:- 首先,不选第
g
组的任何物品:则dp[j]
保持不变(继承只考虑前g-1
组时的状态)。 - 然后,尝试选择第
g
组中的某一件物品i
:前提是背包容量j
必须能容纳该物品 (j >= v[g][i]
)。如果选择这件物品,那么前g-1
组物品占用的容量就是j - v[g][i]
,带来的价值是dp[j - v[g][i]] + w[g][i]
。 - 我们需要遍历第
g
组的所有物品i
,找出选择该组中哪一件物品(或者不选) 能使得dp[j]
最大。
因此,状态转移的核心逻辑是:
for j from V down to 0: // 遍历所有可能的背包容量 (必须逆序!)dp[j] = dp[j]; // 不选第g组任何物品 (保持原值)for each item i in group g: // 遍历第g组的所有物品if j >= v[g][i]:dp[j] = max(dp[j], dp[j - v[g][i]] + w[g][i]); // 尝试选第g组的物品i
重要: 容量
j
的循环必须是逆序(从V
到0
)。这与01背包的逆序原理相同:- 它确保在计算
dp[j]
时,用于参考的dp[j - v[g][i]]
是基于前g-1
组物品的状态,而不是已经被当前组g
更新过的状态。 - 这样避免了同一组内的物品被错误地多次选择(因为组内物品是互斥的)。
- 首先,不选第
-
初始化:
dp[0] = 0
:容量为0时最大价值为0。- 对于
j > 0
,dp[j]
通常初始化为一个非常小的负数(-∞
)或者0
,具体取决于状态定义:- 如果状态定义为“容量恰好为
j
”,则dp[j] = -∞ (j > 0)
,表示初始状态不可达。 - 如果状态定义为“容量不超过
j
”,则dp[j] = 0 (j >= 0)
通常是合理的。
- 如果状态定义为“容量恰好为
-
最终答案:
- 遍历所有容量
j
(0 <= j <= V
),找出dp[j]
的最大值(如果状态定义是“恰好为j
”)。 - 或者直接取
dp[V]
(如果状态定义是“不超过j
”,且初始化合理)。
- 遍历所有容量
伪代码实现:
// 输入:背包容量 V, 组数 G, 每组物品列表 groups[1..G]
// groups[g] 是一个列表,其中每个元素是物品 (v, w)
// 定义 dp 数组:dp[0..V]
// 初始化 (以"不超过j"为例)
for j from 0