贪心算法(Greedy Algorithm) 是一种在每一步选择中都采取当前状态下局部最优解的策略,希望通过一系列局部最优选择最终得到全局最优解的算法设计方法。它的核心思想是“短视但高效”——每一步只关注眼前的最佳选择,不回溯、不全局规划。
贪心算法的核心特点
- 局部最优选择
每一步决策都基于当前信息选择最优选项,不考虑未来的潜在影响。 - 不可回溯
一旦做出选择,不会撤销或重新评估之前的决策。 - 高效性
通常时间复杂度较低(如 O(nlogn)O(nlogn)),适合处理大规模数据。 - 不保证全局最优
只有在问题满足特定性质时,贪心算法才能得到全局最优解。
贪心算法的适用条件
贪心算法有效的关键在于问题满足以下两个性质:
- 贪心选择性质(Greedy Choice Property)
通过局部最优选择可以推导出全局最优解。即:每一步的贪心决策不会被后续步骤推翻。 - 最优子结构(Optimal Substructure)
问题的最优解包含其子问题的最优解。例如,若从 AA 到 CC 的最短路径是 A→B→CA→B→C,则 A→BA→B 和 B→CB→C 也必须是各自段的最短路径。
贪心算法 vs. 动态规划
特性 | 贪心算法 | 动态规划 |
---|---|---|
决策依据 | 仅当前局部最优 | 所有子问题的解组合 |
回溯性 | 无回溯 | 可能需要比较多种子问题的组合 |
时间复杂度 | 通常更低(如 O(nlogn)O(nlogn)) | 通常较高(如 O(n2)O(n2)) |
适用问题 | 满足贪心选择性质的问题 | 具有重叠子问题和最优子结构的问题 |
示例 | 霍夫曼编码、最小生成树(Kruskal) | 背包问题、最长公共子序列 |
经典贪心算法示例
1. 活动选择问题
-
问题:从多个时间重叠的活动中选出最多不冲突的活动。
-
贪心策略:每次选择结束时间最早的活动,为后续留出更多时间。
-
代码片段(按结束时间排序后选择):
function selectActivities(activities) { activities.sort((a, b) => a.end - b.end); // 按结束时间排序 const selected = [activities[0]]; let lastEnd = activities[0].end; for (const act of activities.slice(1)) { if (act.start >= lastEnd) { selected.push(act); lastEnd = act.end; } } return selected; }
2. 霍夫曼编码
- 问题:用最短二进制编码表示字符,使总编码长度最小。
- 贪心策略:优先合并频率最低的节点,构建二叉树。
- 时间复杂度:O(nlogn)O(nlogn)(使用优先队列)。
3. 最小生成树(Kruskal算法)
-
问题:在连通图中选择边,使所有节点连通且总权重最小。
-
贪心策略:按权重从小到大选择边,避免形成环。
-
代码关键步骤:
// 使用并查集(Union-Find)检测环 edges.sort((a, b) => a.weight - b.weight); for (const edge of edges) { if (find(parent, edge.u) !== find(parent, edge.v)) { union(parent, edge.u, edge.v); mst.push(edge); } }
贪心算法的局限性
- 不适用于所有问题
例如,0-1背包问题(物品不可分割)无法用贪心算法得到最优解,但分数背包问题(物品可分割)可以。 - 需严格证明正确性
贪心策略的直观性可能掩盖其潜在错误。例如,若活动选择问题改为“选择时间最短的活动”,则可能无法得到最优解。
如何证明贪心算法的正确性?
- 数学归纳法
证明每一步的贪心选择都能导向全局最优解。 - 交换论证
假设存在一个最优解,通过交换步骤使其逐步与贪心解一致,证明贪心解不劣于最优解。
总结
贪心算法通过局部最优的短视决策,在特定问题中高效达到全局最优。它的威力与风险并存:
- 优势:简单、高效,适合实时系统或大规模数据。
- 挑战:必须严格验证问题是否满足贪心选择性质,否则可能得到次优解。
在应用时,始终先问: “当前问题是否能用贪心策略?如何证明?”