贪心算法并不适用于所有类型的背包问题,它主要适用于分数背包问题,而对于0/1 背包问题,贪心算法通常无法得到最优解。

下面我将分三部分进行讲解:
- 背包问题的三种类型
- 贪心算法解决分数背包问题(Java 实现)
- 为什么贪心算法不适用于 0/1 背包问题
背包问题的三种类型
在讨论算法之前,我们先清晰地定义三种常见的背包问题:
-
0/1 背包问题
- 描述:有
n个物品和一个容量为C的背包,每个物品i有一个重量w[i]和一个价值v[i],每个物品只能选择放入一次或不放入,不能分割。 - 目标:选择一些物品放入背包,使得背包内物品的总重量不超过
C,且总价值最大。 - 例子:你有一个背包,容量 10kg,你有 3 个物品:一个笔记本电脑 (8kg, ¥9000),一个吉他 (5kg, ¥5000),一个音箱 (3kg, ¥3000),你不能把吉他劈成两半放,只能选或不选。
- 描述:有
-
分数背包问题
(图片来源网络,侵删)- 描述:与 0/1 背包问题类似,但每个物品可以被分割成任意小的部分。
- 目标:可以选择物品的一部分放入背包,使得总重量不超过
C,且总价值最大。 - 例子:你有一个背包,容量 10kg,你有 3 个物品:一袋大米 (10kg, ¥50),一桶油 (5kg, ¥100),一袋盐 (2kg, ¥20),你可以拿走半袋大米(5kg, ¥25)。
-
完全背包问题
- 描述:与 0/1 背包问题类似,但每种物品有无限个,可以选择多次。
- 目标:使得总重量不超过
C,且总价值最大。 - 例子:你有一个背包,容量 10kg,你有无尽的金币,每个金币 (1kg, ¥1000),你可以装 10 个金币。
贪心算法解决分数背包问题(Java 实现)
贪心算法的核心思想是:在每一步都做出当前看起来最优的选择,期望最终得到全局最优解。
对于分数背包问题,这个策略非常有效,最优的选择是:优先选择“性价比”(价值/重量)最高的物品,如果背包容量不足以装下整个高性价比物品,就装下它的一部分。
算法步骤:
- 计算性价比:为每个物品计算其单位重量的价值,即
value / weight。 - 排序:将所有物品按照性价比从高到低进行排序。
- 贪心选择:
- 遍历排序后的物品列表。
- 对于当前物品,如果整个物品都能放入剩余的背包容量,则将其全部放入,并更新背包的总价值和剩余容量。
- 如果当前物品只能放入一部分,则计算能放入的比例,放入这部分,然后结束循环(因为背包已满)。
- 如果当前物品完全放不下,则跳过,继续看下一个性价比次高的物品。
Java 代码实现
import java.util.Arrays;
import java.util.Comparator;
// 物品类
class Item {
int weight; // 重量
int value; // 价值
public Item(int weight, int value) {
this.weight = weight;
this.value = value;
}
}
public class FractionalKnapsack {
/**
* 使用贪心算法解决分数背包问题
* @param items 物品数组
* @param capacity 背包容量
* @return 背包中物品的最大总价值
*/
public double getMaxValue(Item[] items, int capacity) {
// 1. 按照性价比 (value/weight) 降序排序
// 使用 Java 8 的 Lambda 表达式进行自定义排序
Arrays.sort(items, new Comparator<Item>() {
@Override
public int compare(Item a, Item b) {
// 注意:为了避免除以 0,并且为了降序,我们比较 b.value/b.weight 和 a.value/a.weight
double ratioA = (double) a.value / a.weight;
double ratioB = (double) b.value / b.weight;
return Double.compare(ratioB, ratioA); // 降序排序
}
});
// 为了更简洁,也可以使用 Lambda 表达式
// Arrays.sort(items, (a, b) -> Double.compare((double)b.value/b.weight, (double)a.value/a.weight));
double totalValue = 0.0; // 总价值
int remainingCapacity = capacity; // 剩余容量
// 2. 遍历排序后的物品列表
for (Item item : items) {
// 如果当前物品可以完全放入背包
if (item.weight <= remainingCapacity) {
totalValue += item.value;
remainingCapacity -= item.weight;
} else {
// 否则,放入物品的一部分
// 能放入的比例 = 剩余容量 / 物品总重量
double fraction = (double) remainingCapacity / item.weight;
totalValue += item.value * fraction;
// 背包已满,跳出循环
break;
}
}
return totalValue;
}
public static void main(String[] args) {
// 示例
Item[] items = {
new Item(10, 60), // 物品1: 重量10, 价值60, 性价比=6
new Item(20, 100), // 物品2: 重量20, 价值100, 性价比=5
new Item(30, 120) // 物品3: 重量30, 价值120, 性价比=4
};
int capacity = 50; // 背包容量
FractionalKnapsack knapsack = new FractionalKnapsack();
double maxValue = knapsack.getMaxValue(items, capacity);
System.out.println("背包的最大价值为: " + maxValue);
// 预期结果: 60 + 100 + (120 * (50-10-30)/30) = 60 + 100 + 40 = 200
}
}
代码解释:

Item类是一个简单的数据类,用于存储物品的重量和价值。getMaxValue方法是核心逻辑。Arrays.sort结合自定义的Comparator,将物品数组按照性价比从高到低排序,这里的关键是比较逻辑Double.compare(ratioB, ratioA),实现了降序排序。- 在
for循环中,我们依次处理每个物品。 - 如果物品能完整装入,就更新总价值和剩余容量。
- 如果只能装入一部分,就计算装入的比例,累加对应的价值,
break,因为背包已经满了。 main方法提供了一个测试用例,并打印了结果。
为什么贪心算法不适用于 0/1 背包问题?
贪心算法的局限性在于它只考虑了眼前的局部最优,而忽略了选择这个物品后对未来的影响,在 0/1 背包问题中,物品不能分割,这个“分割”的选项被拿掉了,导致贪心策略失效。
反例说明
假设我们有以下物品和一个容量为 50 的背包:
| 物品 | 重量 | 价值 | 性价比 |
|---|---|---|---|
| A | 45 | 100 | ~2.22 |
| B | 10 | 60 | 0 |
| C | 10 | 50 | 0 |
使用贪心算法的步骤:
- 排序:按性价比排序,顺序是 B (6.0), C (5.0), A (~2.22)。
- 选择:
- 先选 B,放入背包,总价值 = 60,剩余容量 = 40。
- 再选 C,放入背包,总价值 = 60 + 50 = 110,剩余容量 = 30。
- 再选 A,A 的重量是 45,大于剩余容量 30,无法放入。
- 贪心算法结果:总价值 = 110。
真正的最优解:
-
如果我们不选择性价比最高的 B 和 C,而是选择性价比最低的 A:
- 只选 A,放入背包,总价值 = 100。
- 这个结果比贪心算法的 110 要差,所以还不是最优解。
-
让我们再优化一下,看看有没有比 110 更好的解:
- 选择 B 和 C 的组合,价值是 110。
- 选择 A 的组合,价值是 100。
- 等等,还有没有其他组合?
- 我们可以把 B 和 C 都选上,总价值 110,总重量 20,背包里还有 30 的空间,但没有其他物品可以放了。
- 哦,看来这个例子不够好,让我们换一个更经典的反例。
更经典的反例:
| 物品 | 重量 | 价值 | 性价比 |
|---|---|---|---|
| A | 51 | 52 | ~1.02 |
| B | 50 | 50 | 0 |
| C | 49 | 49 | 0 |
背包容量:100
-
贪心算法:
- 排序后顺序是 A, B, C。
- 选择 A (重量 51 > 100? 不,可以放),总价值 52,剩余容量 49。
- 选择 B (重量 50 > 49? 不行)。
- 选择 C (重量 49 <= 49),总价值 52 + 49 = 101。
- 贪心算法结果:101。
-
最优解:
- 不选 A,选择 B 和 C。
- 总重量 = 50 + 49 = 99 <= 100。
- 总价值 = 50 + 49 = 99。
- 这个例子也不行... 好吧,我承认构造一个完美的反例需要仔细思考。
让我们用最经典的反例:
| 物品 | 重量 | 价值 | 性价比 |
|---|---|---|---|
| A | 4 | 40 | 10 |
| B | 3 | 20 | 66... |
| C | 3 | 20 | 66... |
| D | 2 | 15 | 5 |
| E | 2 | 15 | 5 |
背包容量:10
-
贪心算法:
- 排序:A(10), D(7.5), E(7.5), B(6.66), C(6.66)
- 选 A (重量4),价值40,剩余6。
- 选 D (重量2),价值15,剩余4。
- 选 E (重量2),价值15,剩余2。
- 选 B (重量3 > 2),不行。
- 选 C (重量3 > 2),不行。
- 贪心算法结果:40 + 15 + 15 = 70。
-
最优解:
- 不选 A,选择 D, E, B, C。
- 总重量 = 2 + 2 + 3 + 3 = 10。
- 总价值 = 15 + 15 + 20 + 20 = 70。
- 糟糕,这个例子还是不行,结果一样!
终于找到一个能让贪心算法失败的例子:
| 物品 | 重量 | 价值 | 性价比 |
|---|---|---|---|
| A | 5 | 10 | 0 |
| B | 4 | 9 | 25 |
| C | 4 | 9 | 25 |
| D | 3 | 7 | ~2.33 |
| E | 3 | 7 | ~2.33 |
| F | 3 | 7 | ~2.33 |
背包容量:12
-
贪心算法:
- 排序:D, E, F (2.33), B, C (2.25), A (2.0)
- 选 D (重量3),价值7,剩余9。
- 选 E (重量3),价值7,剩余6。
- 选 F (重量3),价值7,剩余3。
- 选 B (重量4 > 3),不行。
- 选 C (重量4 > 3),不行。
- 选 A (重量5 > 3),不行。
- 贪心算法结果:7 + 7 + 7 = 21。
-
最优解:
- 不选 D, E, F,选择 B, C, A。
- 总重量 = 4 + 4 + 5 = 13 > 12,不行。
- 选择 B, C, 和 F 的组合?4+4+3=11,价值 9+9+7=25。
- 选择 B, C, 和 E 的组合?4+4+3=11,价值 9+9+7=25。
- 选择 B, C, 和 D 的组合?4+4+3=11,价值 9+9+7=25。
- 最优解应该是 25。
在这个例子中,贪心算法得到的结果是 21,而真正的最优解是 25,这是因为贪心算法优先选择了性价比最高的 D, E, F,这占用了 9 的空间,导致无法再装下 B 和 C 这两个虽然性价比稍低但价值总和更高的组合,这个反例清晰地证明了贪心算法在 0/1 背包问题上的局限性。
如何解决 0/1 背包问题?
0/1 背包问题是一个经典的动态规划问题,动态规划通过将问题分解为子问题,并存储子问题的解,从而避免了重复计算,最终能够找到全局最优解。
| 问题类型 | 是否可分割 | 贪心算法 | 常用最优算法 |
|---|---|---|---|
| 分数背包 | 是 | 有效,能得到最优解 | 贪心算法 |
| 0/1 背包 | 否 | 无效,通常得不到最优解 | 动态规划 |
| 完全背包 | 否(但可选多次) | 无效 | 动态规划 |
希望这个详细的解释能帮助你理解贪心算法在背包问题中的应用和限制!
