1. 算法训练营第三十天实战解析
今天要啃下LeetCode上三道经典的贪心算法题目:452.用最少数量的箭引爆气球、435.无重叠区间、763.划分字母区间。这三道题在面试中出现频率极高,也是检验是否真正掌握贪心思想的试金石。我在刷题过程中发现,很多同学卡在"如何证明贪心策略的正确性"这一关,接下来我会用工程化的思维拆解解题过程。
1.1 贪心算法核心思想
贪心算法的本质是"局部最优导致全局最优",但难点在于如何确定这个"局部最优"的选择标准。以区间问题为例,常见的排序维度有:
- 按起点升序
- 按终点升序
- 按区间长度
对于这三道题,终点升序排列往往是解题关键。举个例子,在引爆气球问题中,如果我们按气球的终点排序,那么每支箭都尽可能射在当前最靠左的终点位置,这样能最大化覆盖后续气球。
重要提示:贪心算法的正确性通常需要数学归纳法证明,但在面试中能够给出合理说明即可。实际刷题时建议先验证几个测试用例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. LeetCode 452. 用最少数量的箭引爆气球
2.1 问题重述
在二维空间中有若干个气球,每个气球的直径范围是[xstart, xend]。一支箭可以垂直射出,只要x坐标在气球的直径范围内,气球就会被引爆。求最少需要多少支箭才能引爆所有气球。
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
2.2 解题思路拆解
这道题的贪心策略可以分为四个步骤:
- 将所有区间按照右端点升序排序
- 初始化箭的位置为第一个区间的右端点
- 遍历后续区间:
- 如果当前区间包含箭的位置,跳过
- 否则用新箭射在当前区间的右端点,计数+1
- 返回箭的总数
关键点在于:为什么选择右端点排序?因为这样能保证每支箭尽可能覆盖更多后续区间。想象一下,如果选择左端点排序,可能会在前面的区间浪费箭。
2.3 代码实现与优化
Python标准解法:
python复制def findMinArrowShots(points):
if not points:
return 0
points.sort(key=lambda x: x[1])
arrows = 1
first_end = points[0][1]
for x_start, x_end in points:
if x_start > first_end:
arrows += 1
first_end = x_end
return arrows
时间复杂度分析:
- 排序:O(nlogn)
- 遍历:O(n)
- 总体:O(nlogn)
空间复杂度:O(1) (如果允许修改原数组)
踩坑记录:注意处理空输入的情况。我在第一次提交时忽略了边界条件,导致测试用例[]时报错。
3. LeetCode 435. 无重叠区间
3.1 问题描述
给定一个区间集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
输入:intervals = [[1,2],[2,3],[3,4],[1,3]]
输出:1 (移除[1,3]后其他区间无重叠)
3.2 解题策略对比
这道题与452题看似相似,实则考察角度不同。这里有两种贪心思路:
方案A(与452题类似):
- 按右端点排序
- 记录非重叠区间数
- 总区间数减去非重叠数即为需要移除的数量
方案B(动态规划):
- 按左端点排序
- dp[i]表示前i个区间的最大不重叠数
- 时间复杂度O(n²),不推荐
显然方案A更优。关键在于理解:保留更多的非重叠区间等价于移除更少的区间。
3.3 代码实现细节
Python实现:
python复制def eraseOverlapIntervals(intervals):
if not intervals:
return 0
intervals.sort(key=lambda x: x[1])
count = 1
end = intervals[0][1]
for interval in intervals[1:]:
if interval[0] >= end:
count += 1
end = interval[1]
return len(intervals) - count
常见错误:
- 混淆了区间比较条件(应该是>=而不是>)
- 忘记处理空输入的情况
- 错误计算需要移除的数量(应该是总数-保留数)
4. LeetCode 763. 划分字母区间
4.1 问题特点分析
这道题与前两道风格不同,属于字符串处理与贪心结合的类型。要求将字符串划分为尽可能多的片段,使得每个字母最多出现在一个片段中。
输入:S = "ababcbacadefegdehijhklij"
输出:[9,7,8] (划分成"ababcbaca", "defegde", "hijhklij")
4.2 解题关键突破点
解决这个问题需要两个核心步骤:
- 统计每个字母最后出现的位置
- 遍历时维护当前片段的边界
例如字母a的最后位置是8,那么第一个片段至少要包含到第8个字符。当遍历到8时,如果所有之前字母的最后位置都不超过8,则可以划分。
4.3 完整实现与测试
Python解决方案:
python复制def partitionLabels(S):
last = {c: i for i, c in enumerate(S)}
start = end = 0
res = []
for i, c in enumerate(S):
end = max(end, last[c])
if i == end:
res.append(end - start + 1)
start = i + 1
return res
时间复杂度:O(n),只需要两次遍历
空间复杂度:O(1),因为字母表大小固定
调试技巧:可以先用小例子测试,比如"abac"应该返回[3,1]。我在最初实现时忽略了更新start的位置,导致结果错误。
5. 贪心算法问题通用解法总结
通过这三道题,我们可以提炼出解决区间类贪心问题的通用模式:
-
排序阶段:
- 确定排序维度(通常按起点或终点)
- 考虑是否需要处理特殊情况(如空输入)
-
遍历阶段:
- 初始化关键变量(如计数、边界等)
- 设计合理的比较条件
- 注意边界条件的处理
-
验证阶段:
- 用简单测试用例验证(如2-3个元素的输入)
- 检查极端情况(空输入、全等区间等)
对于面试准备,建议将这三道题作为一组来练习,比较它们的异同点。我在准备大厂面试时发现,面试官常常会连续问这几道题,考察候选人是否能举一反三。
6. 常见错误与调试技巧
6.1 区间端点比较错误
在452和435题中,区间比较时容易混淆开闭区间。例如:
- 引爆气球问题中,箭可以等于xstart或xend
- 无重叠区间问题中,[1,2]和[2,3]不算重叠
建议在代码注释中明确写出比较条件,避免混淆。
6.2 排序维度选择错误
我曾尝试用起点排序解决引爆气球问题,结果发现需要更复杂的逻辑。经验是:
- 需要覆盖最多后续区间时,用终点排序
- 需要处理区间合并时,用起点排序
6.3 边界条件处理
三道题都需要考虑:
- 空输入处理
- 单元素输入处理
- 全等区间的情况
7. 性能优化与进阶思考
7.1 算法优化空间
虽然这些问题的标准解法已经最优,但可以思考:
- 如果区间已经部分排序,如何利用这一信息?
- 并行计算是否可能?(对于极大区间集合)
7.2 实际工程应用
这类算法在以下场景有实际应用:
- 日历调度系统(会议室安排)
- 任务调度优化
- 资源分配问题
例如,引爆气球问题可以转化为"如何用最少的监控摄像头覆盖所有路段"。
8. 测试用例设计指南
为了验证代码正确性,建议设计以下几类测试用例:
-
常规情况:
- 452:[[10,16],[2,8],[1,6],[7,12]] → 2
- 435:[[1,2],[2,3],[3,4],[1,3]] → 1
- 763:"ababcbacadefegdehijhklij" → [9,7,8]
-
边界情况:
- 空输入
- 单元素输入
- 所有区间相同
-
极端情况:
- 大量重复区间
- 超大范围区间
9. 同类题目推荐与扩展
为了巩固贪心算法,建议继续练习:
- 406.根据身高重建队列
- 605.种花问题
- 122.买卖股票的最佳时机II
这些题目都体现了"局部最优导致全局最优"的思想,但应用场景各不相同。我在刷完这些题后发现,贪心算法的难点不在于编码,而在于如何识别问题适用贪心策略。
