背包问题是组合优化问题中最经典的一类问题。它有多个变体,包括 0/1 背包、完全背包、多重背包和分组背包。下面详细讲解这几种背包问题及其解决方案。
1. 0/1 背包问题
问题描述
给定 n
个物品,每个物品有一个重量和一个价值,背包的最大容量为 W
。每个物品只能选择放入背包或不放入背包,如何选择物品使得背包中的总价值最大化,并且不超过背包容量 W
?
动态规划解决方案
-
定义状态:
dp[i][w]
表示前i
个物品中,总重量不超过w
时的最大价值。
-
状态转移方程:
- 如果第
i
个物品不放入背包:dp[i][w] = dp[i-1][w]
- 如果第
i
个物品放入背包:dp[i][w] = dp[i-1][w - weight[i-1]] + value[i-1]
- 综合状态转移方程:
dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i-1]] + value[i-1])
- 如果第
-
初始化:
dp[0][w] = 0
,表示没有物品时,背包的最大价值为 0。
-
计算:
- 使用嵌套循环填充
dp
数组,从i=1
到n
,从w=0
到W
。
- 使用嵌套循环填充
示例代码
function knapsack($weights, $values, $capacity) {
$n = count($weights);
$dp = array_fill(0, $n + 1, array_fill(0, $capacity + 1, 0));
for ($i = 1; $i <= $n; $i++) {
for ($w = 0; $w <= $capacity; $w++) {
if ($weights[$i - 1] <= $w) {
$dp[$i][$w] = max($dp[$i - 1][$w], $dp[$i - 1][$w - $weights[$i - 1]] + $values[$i - 1]);
} else {
$dp[$i][$w] = $dp[$i - 1][$w];
}
}
}
return $dp[$n][$capacity];
}
2. 完全背包问题
问题描述
在完全背包问题中,每个物品可以放入背包多次(即无限次)。如何选择物品使得背包中的总价值最大化,并且不超过背包容量 W
?
动态规划解决方案
-
定义状态:
dp[w]
表示背包容量为w
时的最大价值。
-
状态转移方程:
- 如果第
i
个物品可以放入背包:dp[w] = max(dp[w], dp[w - weight[i-1]] + value[i-1])
- 如果第
-
初始化:
dp[0] = 0
,表示背包容量为 0 时的最大价值为 0。
-
计算:
- 使用一层循环填充
dp
数组,从w=0
到W
。
- 使用一层循环填充
示例代码
function completeKnapsack($weights, $values, $capacity) {
$n = count($weights);
$dp = array_fill(0, $capacity + 1, 0);
for ($w = 0; $w <= $capacity; $w++) {
foreach ($weights as $i => $weight) {
if ($weight <= $w) {
$dp[$w] = max($dp[$w], $dp[$w - $weight] + $values[$i]);
}
}
}
return $dp[$capacity];
}
3. 多重背包问题
问题描述
每个物品有一个数量限制,即每个物品最多可以放入背包指定次数。如何选择物品使得背包中的总价值最大化,并且不超过背包容量 W
?
动态规划解决方案
-
定义状态:
dp[w]
表示背包容量为w
时的最大价值。
-
状态转移方程:
- 如果第
i
个物品可以放入背包:dp[w] = max(dp[w], dp[w - k * weight[i-1]] + k * value[i-1])
,其中k
为物品的数量限制。
- 如果第
-
初始化:
dp[0] = 0
,表示背包容量为 0 时的最大价值为 0。
-
计算:
- 使用一层循环填充
dp
数组,从w=0
到W
。
- 使用一层循环填充
示例代码
function multipleKnapsack($weights, $values, $counts, $capacity) {
$n = count($weights);
$dp = array_fill(0, $capacity + 1, 0);
for ($i = 0; $i < $n; $i++) {
$weight = $weights[$i];
$value = $values[$i];
$count = $counts[$i];
for ($w = $capacity; $w >= $weight; $w--) {
for ($k = 1; $k <= $count; $k++) {
if ($w >= $k * $weight) {
$dp[$w] = max($dp[$w], $dp[$w - $k * $weight] + $k * $value);
} else {
break;
}
}
}
}
return $dp[$capacity];
}
4. 分组背包问题
问题描述
物品被分为若干组,每组的物品可以选择放入背包,但每组只能选择一个物品。如何选择物品使得背包中的总价值最大化,并且不超过背包容量 W
?
动态规划解决方案
-
定义状态:
dp[w]
表示背包容量为w
时的最大价值。
-
状态转移方程:
- 对于每一组物品,遍历选择其中一个物品,更新
dp
数组。
- 对于每一组物品,遍历选择其中一个物品,更新
-
初始化:
dp[0] = 0
,表示背包容量为 0 时的最大价值为 0。
-
计算:
- 使用一层循环填充
dp
数组,从w=0
到W
。
- 使用一层循环填充
示例代码
function groupKnapsack($groups, $capacity) {
$dp = array_fill(0, $capacity + 1, 0);
foreach ($groups as $group) {
$temp = $dp;
foreach ($group as $item) {
$weight = $item['weight'];
$value = $item['value'];
for ($w = $capacity; $w >= $weight; $w--) {
$temp[$w] = max($temp[$w], $dp[$w - $weight] + $value);
}
}
$dp = $temp;
}
return $dp[$capacity];
}
// Example usage:
$groups = [
[['weight' => 2, 'value' => 3], ['weight' => 3, 'value' => 4]],
[['weight' => 4, 'value' => 5], ['weight' => 5, 'value' => 6]],
[['weight' => 1, 'value' => 2], ['weight' => 2, 'value' => 3]]
];
$capacity = 8;
echo groupKnapsack($groups, $capacity); // Output: 12
总结
背包问题有多种变体,每种变体有其独特的挑战和解决方案。通过理解这些变体的定义、状态转移方程和计算方法,你可以根据具体的需求选择合适的算法来优化解决方案。动态规划是解决这些问题的常用技术,通过分解问题和缓存中间结果来提高效率。