1. 贪心算法基础认知
第一次接触贪心算法是在解决力扣的"分发饼干"问题时。当时我尝试用暴力解法穷举所有可能的分配方案,结果时间复杂度直接爆表。直到看到题解中那个简洁优雅的贪心解法——先排序再匹配,才恍然大悟原来算法可以如此精妙。
贪心算法(Greedy Algorithm)就像我们生活中做决策时的直觉思维:每一步都选择当前看起来最优的选项,期望通过局部最优的累积达到全局最优。这种"活在当下"的策略虽然不保证在所有情况下都能得到最佳结果,但在特定场景下往往能给出令人满意的解决方案。
与动态规划需要保存子问题解不同,贪心算法做出选择后就不会回退。这种不可回退的特性决定了它适用于具有"贪心选择性质"的问题——即局部最优解能导致全局最优解。典型的应用场景包括:
- 霍夫曼编码(数据压缩)
- 最小生成树(Prim/Kruskal算法)
- 最短路径(Dijkstra算法)
- 任务调度问题
- 零钱兑换问题
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 贪心算法的核心特征
2.1 贪心选择性质
贪心算法的有效性建立在问题的两个关键性质上。首先是贪心选择性质(Greedy Choice Property),即通过局部最优选择能够构建全局最优解。以经典的"活动选择问题"为例:
假设有一系列活动,每个活动有开始和结束时间。如何选择最多数量的互不冲突的活动?贪心策略是按结束时间排序,每次选择结束最早且不与已选活动冲突的活动。这种策略之所以有效,是因为早结束的活动能为后续留下更多时间。
python复制def activity_selection(start, end):
activities = sorted(zip(start, end), key=lambda x: x[1])
selected = [activities[0]]
for current in activities[1:]:
if current[0] >= selected[-1][1]:
selected.append(current)
return selected
2.2 最优子结构
第二个关键性质是最优子结构(Optimal Substructure),即问题的最优解包含其子问题的最优解。在"找零钱问题"中,如果用贪心策略(每次选最大面额),必须确保大面额是小面额的倍数(如美国硬币体系)。否则可能得不到最优解——这就是为什么贪心算法不适用于任意面额的找零问题。
重要提示:在使用贪心算法前,必须证明问题具有这两个性质。常见的证明方法包括交换论证(Exchange Argument)和数学归纳法。
3. 典型贪心问题解析
3.1 区间调度问题
力扣第435题"无重叠区间"是区间问题的变种。要求移除最少数量的区间使剩余区间互不重叠。贪心策略是按结束时间排序,记录非重叠区间数,最后用总数减去非重叠数。
python复制def erase_overlap_intervals(intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[1])
count = 1
end = intervals[0][1]
for i in range(1, len(intervals)):
if intervals[i][0] >= end:
count += 1
end = intervals[i][1]
return len(intervals) - count
这个解法的时间复杂度是O(nlogn),主要来自排序操作。空间复杂度O(1)。
3.2 跳跃游戏问题
力扣第55题"跳跃游戏"要求判断能否从数组起点跳到终点,数组元素表示最大跳跃长度。贪心策略是维护当前能到达的最远位置:
python复制def can_jump(nums):
max_reach = 0
for i in range(len(nums)):
if i > max_reach:
return False
max_reach = max(max_reach, i + nums[i])
return True
进阶版是第45题"跳跃游戏II",要求计算最少跳跃次数。这时需要记录当前步数能到达的边界和下一步能到达的最远位置:
python复制def jump(nums):
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums)-1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumps
4. 贪心算法的局限性
4.1 非全局最优案例
贪心算法最著名的失败案例是0-1背包问题。假设物品可以分割(分数背包),贪心按价值密度排序是正确的。但对于不可分割的0-1背包,贪心可能得到很差的结果:
code复制物品列表:[(60,10), (100,20), (120,30)] 背包容量50
贪心选择:选60和100,总价值160
最优解:选100和120,总价值220
4.2 反悔贪心算法
为了弥补传统贪心的缺陷,发展出了"反悔贪心"(Regret Greedy)算法。基本思路是保留一个候选集合,当发现之前的选择不是最优时进行"反悔"。典型实现通常使用优先队列:
python复制import heapq
def maxPerformance(n, speed, efficiency, k):
engineers = sorted(zip(efficiency, speed), reverse=True)
min_heap = []
total_speed = 0
res = 0
for e, s in engineers:
if len(min_heap) == k:
total_speed -= heapq.heappop(min_heap)
heapq.heappush(min_heap, s)
total_speed += s
res = max(res, total_speed * e)
return res % (10**9 + 7)
这个解法用于解决力扣第1383题"最大的团队表现值",时间复杂度O(nlogk)。
5. 贪心算法实战技巧
5.1 问题转化技巧
许多问题需要先转化为贪心可解的形式。例如力扣第452题"用最少数量的箭引爆气球",本质是找最多的不重叠区间:
python复制def find_min_arrows(points):
if not points: return 0
points.sort(key=lambda x: x[1])
arrows = 1
end = points[0][1]
for s, e in points:
if s > end:
arrows += 1
end = e
return arrows
5.2 边界条件处理
贪心算法特别容易忽略边界条件。在解决力扣第134题"加油站"时,必须检查总油量是否足够:
python复制def can_complete_circuit(gas, cost):
if sum(gas) < sum(cost):
return -1
start = total = current = 0
for i in range(len(gas)):
current += gas[i] - cost[i]
if current < 0:
start = i + 1
current = 0
return start
5.3 贪心与其他算法的结合
在实际工程中,贪心常与其他算法结合。例如Dijkstra算法本质是贪心+动态规划,Prim算法是贪心+优先队列。在解决力扣第621题"任务调度器"时,贪心策略需要结合数学分析:
python复制def least_interval(tasks, n):
freq = [0] * 26
for t in tasks:
freq[ord(t) - ord('A')] += 1
freq.sort()
max_freq = freq[-1]
idle = (max_freq - 1) * n
for i in range(24, -1, -1):
if freq[i] == 0:
break
idle -= min(max_freq - 1, freq[i])
return len(tasks) + max(0, idle)
6. 贪心算法进阶训练
6.1 典型问题分类
根据我的刷题经验,贪心问题大致可分为以下几类:
- 分配问题:如分发饼干、分糖果(力扣455、135题)
- 区间问题:如合并区间、删除重叠区间(力扣56、435题)
- 跳跃问题:如跳跃游戏系列(力扣55、45题)
- 调度问题:如任务调度器(力扣621题)
- 字符串问题:如划分字母区间(力扣763题)
- 买卖问题:如买卖股票最佳时机(力扣122题)
6.2 力扣精选题目
以下是我整理的贪心算法进阶路线(按难度排序):
-
简单难度:
-
- 分发饼干
-
- 柠檬水找零
-
- K次取反后最大化的数组和
-
-
中等难度:
-
- 摆动序列
-
- 单调递增的数字
-
- 加油站
-
-
困难难度:
-
- 分发糖果
-
- 去除重复字母
-
- IPO
-
6.3 贪心算法证明方法
要真正掌握贪心算法,必须学会证明其正确性。常见证明方法包括:
- 交换论证:假设存在更优解,通过交换元素得到矛盾
- 数学归纳法:证明贪心选择在每一步都保持最优
- 决策树分析:比较贪心路径与其他路径的优劣
以"活动选择问题"为例,用交换论证证明:
假设贪心解为A,最优解为O。找到第一个不同的选择,用A的活动替换O的活动,新解仍有效且活动数不变。重复此过程可将O转化为A,证明A也是最优解。
7. 贪心算法工程实践
7.1 实际应用场景
在我的开发生涯中,曾多次应用贪心算法解决实际问题:
- 资源调度系统:使用类似"活动选择"的算法优化服务器任务调度
- 缓存淘汰策略:实现类LRU的贪心淘汰算法
- 网络路由选择:Dijkstra算法的变种用于内部网络路由
- 数据压缩:霍夫曼编码实现日志压缩存储
7.2 性能优化技巧
贪心算法虽然通常高效,但在大数据量下仍需优化:
- 优先队列优化:使用堆代替排序(如Python的heapq模块)
- 早期终止:在满足条件时提前退出循环
- 空间优化:原地操作代替额外存储
- 并行处理:对独立子问题使用多线程
例如在解决"合并区间"问题时,可以优化空间使用:
python复制def merge(intervals):
intervals.sort(key=lambda x: x[0])
merged = []
for interval in intervals:
if not merged or merged[-1][1] < interval[0]:
merged.append(interval)
else:
merged[-1][1] = max(merged[-1][1], interval[1])
return merged
7.3 调试与验证
贪心算法容易因边界条件出错,建议采用:
- 小数据测试:手动计算预期结果
- 极端案例:空输入、完全重叠区间等
- 可视化调试:绘制区间图辅助理解
- 对拍测试:与暴力解法结果对比
例如测试跳跃游戏时,应该考虑:
- 空数组
- 单元素数组
- 全为1的数组
- 包含0的数组
- 无法到达终点的案例
