算法之贪心(简)
贪心算法(Greedy Algorithm)
贪心算法是一种局部最优导向的启发式算法,核心思想是:在每一步决策中,都选择当前状态下的最优解(局部最优),并期望通过一系列局部最优的选择,最终得到全局最优解。
需要注意的是,贪心算法并非适用于所有问题 —— 它的有效性依赖于问题是否具备「贪心选择性质」(局部最优决策能导出全局最优)和「最优子结构性质」(全局最优解包含子问题的最优解)。若问题不满足这两个性质,贪心算法可能只能得到近似解。
一、核心特性
1. 贪心选择性质
每次选择仅依赖当前局部信息,不回溯、不考虑未来决策的影响。例如,找零问题中,每次优先选面值最大的硬币,就是典型的局部最优选择。
2. 最优子结构性质
全局最优解可以通过一系列局部最优解的组合得到。例如,最短路径问题中,从起点到终点的最短路径,包含了路径上任意两个中间节点之间的最短路径。
3. 无后效性
一旦做出选择,就无法更改,后续决策仅基于当前的选择结果,不依赖之前未选择的路径。
二、算法步骤
- 问题分解:将原问题分解为多个相互独立的子问题(或步骤)。
- 局部最优选择:对每个子问题,选择当前状态下的最优解(需定义明确的 “最优” 标准,如最大、最小、代价最低等)。
- 合并结果:将所有局部最优解组合,形成原问题的最终解。
三、经典应用场景
1. 找零问题(硬币找零)
- 问题:给定不同面值的硬币(如 1 元、5 元、10 元、20 元),用最少的硬币数量凑出指定金额。
- 贪心策略:每次优先选择面值最大的硬币,直到凑出目标金额。
- 适用条件:硬币系统需满足 “贪心选择性质”(如人民币、美元的硬币系统);若硬币面值特殊(如 1 元、3 元、4 元),贪心可能失效(例如凑 6 元:贪心选 4+1+1=3 枚,最优解是 3+3=2 枚)。
2. 活动选择问题
- 问题:给定多个互不重叠的活动(每个活动有开始时间和结束时间),选择最多的活动,使得它们互不冲突。
- 贪心策略:优先选择结束时间最早的活动,为后续活动预留更多时间。
- 示例:活动集合
[(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16)]
,最优解是选择(1,4), (5,7), (8,11), (12,16)
,共 4 个活动。
3. 最小生成树(MST)
- 问题:带权连通无向图中,找到总权重最小的生成树(包含所有顶点,边数为 n-1,无环)。
- 贪心策略:
- Prim 算法:从起始顶点出发,每次选择连接 “已选顶点集” 和 “未选顶点集” 的权重最小的边。
- Kruskal 算法:按边权重升序排序,每次选择不形成环的权重最小边。
4. 单源最短路径(Dijkstra 算法)
- 问题:带权有向图中,找到从起点到其他所有顶点的最短路径(边权重非负)。
- 贪心策略:每次选择当前距离起点最近的未访问顶点,更新其邻接顶点的距离。
5. 哈夫曼编码(Huffman Coding)
- 问题:为字符设计前缀编码,使得总编码长度最短(压缩数据)。
- 贪心策略:每次选择出现频率最低的两个节点合并,生成新节点,重复直到形成一棵哈夫曼树,树的路径即为字符的编码(左 0 右 1 或反之)。
四、优缺点
优点
- 效率高:步骤简单,无需回溯,时间复杂度通常较低(如 O (n log n),主要来自排序)。
- 实现简洁:逻辑直观,代码容易编写和维护。
缺点
- 局限性强:仅适用于具备 “贪心选择性质” 和 “最优子结构” 的问题,否则可能得到次优解。
- 无法回溯:一旦做出决策就无法更改,若局部最优选择导致后续陷入死局,无法调整。
五、代码示例(活动选择问题)
以 “选择最多不冲突活动” 为例,展示贪心算法的实现:
cpp
运行
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;// 活动结构体:开始时间start,结束时间end
struct Activity {int start;int end;
};// 排序规则:按结束时间升序排列
bool compare(const Activity& a, const Activity& b) {return a.end < b.end;
}// 贪心选择最多不冲突活动
vector<Activity> selectMaxActivities(vector<Activity>& activities) {vector<Activity> result;if (activities.empty()) return result;// 1. 按结束时间排序sort(activities.begin(), activities.end(), compare);// 2. 选择第一个活动(结束时间最早)result.push_back(activities[0]);int lastEnd = activities[0].end;// 3. 遍历后续活动,选择与上一个活动不冲突的(开始时间>=上一个结束时间)for (int i = 1; i < activities.size(); i++) {if (activities[i].start >= lastEnd) {result.push_back(activities[i]);lastEnd = activities[i].end;}}return result;
}int main() {vector<Activity> activities = {{1,4}, {3,5}, {0,6}, {5,7}, {3,9},{5,9}, {6,10}, {8,11}, {8,12}, {2,14}, {12,16}};vector<Activity> selected = selectMaxActivities(activities);cout << "选择的活动数量:" << selected.size() << endl;cout << "活动详情:" << endl;for (auto& act : selected) {cout << "(" << act.start << ", " << act.end << ")" << endl;}return 0;
}
输出结果
plaintext
选择的活动数量:4
活动详情:
(1, 4)
(5, 7)
(8, 11)
(12, 16)
六、贪心算法与其他算法的区别
1. 与动态规划的区别
- 贪心:局部最优导向,无回溯,适合子问题独立的场景。
- 动态规划:全局最优导向,通过存储子问题的最优解避免重复计算,适合子问题重叠、需要回溯的场景(如背包问题)。
2. 与回溯法的区别
- 贪心:不探索所有可能路径,仅选局部最优,效率高但可能非全局最优。
- 回溯法:探索所有可能路径,能找到全局最优,但时间复杂度高(如排列组合问题)。
总结
贪心算法是一种高效的启发式算法,核心是 “局部最优导出全局最优”。它适用于特定类型的问题(如活动选择、最小生成树、哈夫曼编码等),在工程实践中常用于追求高效解的场景。使用时需先验证问题是否具备贪心选择性质,否则需考虑动态规划、回溯法等其他算法。