8.0 KiB
tags, create time
| tags | create time | |||
|---|---|---|---|---|
|
2026-06-10 11:11 |
贪心算法
概述
贪心算法(Greedy Algorithm)是一种在每一步都做出局部最优选择的算法策略。它不考虑全局情况,只关注当前看来最好的选择,并期望通过局部最优的累积达到全局最优。贪心算法简洁高效,但并非对所有问题都适用——它需要严格满足贪心选择性质和最优子结构。
正文
一、贪心算法基本思想
贪心算法的核心是**"短视"策略**:每一步都做出当前看起来最优的选择,不回头、不后悔。如果一个问题具有合适的结构,贪心算法可以得到全局最优解。
flowchart TD
Define["Step 1: Define the problem"]
Strategy["Step 2: Formulate greedy strategy"]
Proof["Step 3: Prove greedy choice property"]
Implement["Step 4: Implement the algorithm"]
Optimal["Obtain the optimal solution"]
Define --> Strategy
Strategy --> Proof
Proof --> Implement
Implement --> Optimal
二、两个核心性质
贪心算法能够得到最优解,需要同时满足以下两个性质:
- 贪心选择性质(Greedy Choice Property):通过做出局部最优选择,能够到达全局最优解。即局部最优可以推出全局最优。
- 最优子结构(Optimal Substructure):问题的最优解包含其子问题的最优解。
[!question] 贪心算法的两个核心性质是什么?如何理解它们之间的关系? 贪心选择性质保证"每步选最优"是正确的策略,最优子结构保证"子问题的最优能组合成全局最优"。两者缺一不可:没有贪心选择性质,局部最优不等于全局最优;没有最优子结构,子问题的解无法组合。
三、贪心适用条件
- 具备贪心选择性质
- 具备最优子结构
- 贪心策略一旦选定,不回溯(不做改变)
[!warning] 贪心不是万能的 贪心算法只适用于满足上述两个性质的问题。对于不满足的问题(如0-1背包、旅行商问题),贪心可能得到次优甚至很差的解。
四、经典应用
4.1 活动选择/调度问题
问题:有 n 个活动,每个活动有开始时间 sᵢ 和结束时间 fᵢ。选择最多的互不冲突的活动。
贪心策略:按结束时间从早到晚排序,优先选择结束时间早的活动。
#include <vector>
#include <algorithm>
using namespace std;
struct Activity {
int start, finish;
};
// 活动选择问题:按结束时间贪心选择最多的互不冲突活动
vector<int> activitySelection(vector<Activity>& activities) {
// 按结束时间从早到晚排序
sort(activities.begin(), activities.end(),
[](const Activity& a, const Activity& b) {
return a.finish < b.finish;
});
vector<int> selected;
selected.push_back(0); // 选择第一个结束最早的
int lastFinish = activities[0].finish;
for (int i = 1; i < (int)activities.size(); i++) {
if (activities[i].start >= lastFinish) {
selected.push_back(i); // 记录被选中的活动下标
lastFinish = activities[i].finish;
}
}
return selected;
}
解释:选择结束时间最早的活动,可以为后续活动留出最多的时间空间。每选择一个活动,就排除与其冲突的活动,然后继续在剩余活动中选结束最早的。
[!question] 为什么活动选择问题要按结束时间排序,而不是按开始时间排序? 按开始时间排序可能导致选择了一个开始很早但持续时间很长的活动,从而排除了多个短活动。按结束时间排序能最大化"腾出时间",让更多活动有机会被选中。
4.2 分数背包问题(Fractional Knapsack)
问题:有 n 个物品和一个容量为 W 的背包。每个物品有重量 wᵢ 和价值 vᵢ。可以取物品的一部分。求最大价值。
贪心策略:按价值密度(vᵢ/wᵢ)从高到低排序,优先取价值密度最高的物品。
#include <vector>
#include <algorithm>
using namespace std;
struct Item {
double weight, value;
};
// 分数背包问题:按价值密度贪心,可取物品的一部分
double fractionalKnapsack(vector<Item>& items, double W) {
// 按价值密度(value/weight)从高到低排序
sort(items.begin(), items.end(),
[](const Item& a, const Item& b) {
return (a.value / a.weight) > (b.value / b.weight);
});
double totalValue = 0;
double remaining = W;
for (const auto& item : items) {
if (item.weight <= remaining) {
totalValue += item.value; // 整个物品装入
remaining -= item.weight;
} else {
totalValue += item.value * (remaining / item.weight); // 装入一部分
break;
}
}
return totalValue;
}
解释:因为可以取物品的一部分,所以优先取"性价比"最高的物品一定能得到最优解。剩余容量不够装整个物品时,取一部分即可。
4.3 0-1背包问题
问题:与分数背包类似,但每个物品只能取或不取(不可分割)。
[!warning] 0-1背包不能用贪心! 0-1背包问题中,贪心算法不能保证得到最优解。因为物品不可分割,按价值密度贪心可能选择了一个大而重的物品,而放弃多个小但总价值更高的物品。0-1背包需要用动态规划求解。
[!question] 为什么分数背包可以用贪心而0-1背包不行? 分数背包可以取物品的一部分,这意味着贪心策略不会"浪费"容量——即使当前物品装不下整个,也可以装一部分。而0-1背包要么全装要么不装,贪心选择可能导致容量浪费,无法达到最优。
4.4 水桶打水排队问题
问题:n个人各自拿着不同容量的水桶排队打水,水龙头只有一个。如何安排排队顺序使所有人等待时间之和最短?
贪心策略:按桶容量从小到大排序。
解释:桶小的人打水快,让他先打可以减少后面所有人的等待时间。这是一种"服务时间最短优先"的策略。
- 如果按桶容量从小到大排队,总等待时间为:
\sum_{i=1}^{n} (n-i+1) \cdot t_i其中 tᵢ 是第 i 个人的打水时间。由于 t₁ ≤ t₂ ≤ ... ≤ tₙ,这个加权和最小。
五、贪心 vs 动态规划
| 特性 | 贪心算法 | 动态规划 |
|---|---|---|
| 决策方式 | 每步选最优,不回溯 | 考虑所有子问题的解 |
| 子问题关系 | 不需要存储子问题解 | 需要存储子问题解(记忆化) |
| 效率 | 通常更高 | 通常更低(但保证最优) |
| 最优性 | 不一定(取决于问题性质) | 保证最优 |
| 适用条件 | 贪心选择性质+最优子结构 | 最优子结构+重叠子问题 |
| 典型应用 | 活动选择、分数背包 | 0-1背包、最短路径、LCS |
flowchart LR
Problem["Optimization Problem"]
GreedyCheck{"Greedy choice property holds?"}
Greedy["Use Greedy Algorithm"]
DPCheck{"Overlapping subproblems?"}
DP["Use Dynamic Programming"]
Brute["Other methods"]
Problem --> GreedyCheck
GreedyCheck --> |"Yes"| Greedy
GreedyCheck --> |"No"| DPCheck
DPCheck --> |"Yes"| DP
DPCheck --> |"No"| Brute
六、贪心不适用的情况
- 0-1背包问题:物品不可分割,贪心可能浪费容量
- 旅行商问题(TSP):贪心选择最近城市不能保证全局最短路径
- 一般图着色问题:贪心着色不一定用最少颜色
[!tip] 判断能否使用贪心的思路
- 先提出一个贪心策略
- 尝试证明:对任意输入,贪心选择都能导致最优解
- 如果证明失败,说明贪心不适用,考虑动态规划或回溯