在解决 LeetCode 的「452. 用最少数量的箭引爆气球」问题时,我们需要在保证射爆所有气球的前提下,找到最少的弓箭数量。本文将结合具体代码,深入解析该问题的贪心解法,用两种不同的循环写法来达成目的并揭示其与经典区间问题(Leetcode 435.区间重叠问题)的异同。
给定气球区间的数组 points
,其中每个区间表示气球的水平直径范围。弓箭可以从任意 x 坐标垂直射出,若该坐标在气球直径范围内,则气球被引爆。求引爆所有气球所需的最小弓箭数。
示例:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:在 x=6 处射箭引爆 [2,8] 和 [1,6],在 x=11 处射箭引爆 [10,16] 和 [7,12]
#include
#include
#include
using namespace std;
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
if (points.empty()) return 0;
// 按区间右端点升序排序
sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
return a[1] < b[1];
});
int arrows = 0;
long last_end = LONG_MIN; // 记录上一支箭的位置
for (int i = 0; i < points.size(); ++i) {
// 当前区间左端点 > 上一箭位置,需要新箭
if (points[i][0] > last_end) {
arrows++;
last_end = points[i][1]; // 射箭位置设为当前区间右端点
}
}
return arrows;
}
};
排序预处理
sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
return a[1] < b[1];
});
[[10,16],[2,8],[1,6],[7,12]]
排序后为 [[1,6], [2,8], [7,12], [10,16]]
。贪心遍历
for (int i = 0; i < points.size(); ++i) {
if (points[i][0] > last_end) {
arrows++;
last_end = points[i][1];
}
}
points[i][0]
> 上一箭位置 last_end
,说明需要新箭。points[i][1]
,以覆盖后续可能重叠的区间。k
个区间已被最优覆盖,第 k+1
个区间若与当前射箭位置无重叠,则必须新增箭,而选择其右端点可最大化覆盖后续区间。问题 | 合并区间问题 | 气球弓箭问题 |
---|---|---|
排序方式 | 按左端点升序排序 | 按右端点升序排序 |
目标 | 合并重叠区间 | 寻找覆盖所有区间的最少点 |
关键条件 | 当前左端点 <= 上一右端点 | 当前左端点 > 上一射箭位置 |
0
。1
。[[1,2],[2,3]]
需要 2 支箭(端点相接不算重叠)。输入:[[1,2],[3,4],[5,6],[7,8]]
输出:4 (无重叠,需4支箭)
输入:[[1,2],[2,3],[3,4],[4,5]]
输出:2 (射在2和4处)
通过按右端点排序和贪心遍历,我们以 O(n log n) 的时间复杂度高效解决了问题。代码简洁且覆盖所有边界条件,体现了贪心算法“局部最优即全局最优”的核心思想。理解排序策略与射箭位置更新的逻辑,是掌握此类区间覆盖问题的关键。
在解决「452. 用最少数量的箭引爆气球」问题时,使用按右端点升序排序的方法展现了独特的优势。不过其实作者自己写的时候也在想到底怎么把这个思路给说通,我们分析问题能分析出应该按右端点排序,大概是因为这样排我们满足从局部最优→到整体最优的贪心思想。So,以下从排序策略的核心逻辑、贪心选择的最优性、覆盖范围的效率三个方面详细分析这种方法的精妙之处:
将区间按右端点升序排列,例如:
原始输入:[[10,16], [2,8], [1,6], [7,12]]
排序后:[[1,6], [2,8], [7,12], [10,16]]
每次选择当前区间的右端点作为射箭位置:
[[1,6], [2,8], [7,12], [10,16]]
中,第一支箭射在 6
,覆盖 [1,6]
和 [2,8]
;第二支箭射在 12
,覆盖 [7,12]
和 [10,16]
。k
个区间已被最优覆盖,处理第 k+1
个区间时,若其左端点超出当前箭的位置,必须新增箭。选择其右端点射箭,能覆盖所有可能重叠的后续区间。题目规定端点相接不算重叠(如 [1,2]
和 [2,3]
):
2
处无法覆盖下一个区间 [2,3]
(左端点等于当前射箭位置),需新增箭。2
处可能覆盖后续区间,但需频繁调整射箭位置。以输入 [[1,5], [2,3], [4,7]]
为例:
[[2,3], [1,5], [4,7]]
3
处覆盖 [2,3]
和 [1,5]
,射在 7
处覆盖 [4,7]
→ 2支箭。[[1,5], [2,3], [4,7]]
5
处仅覆盖 [1,5]
和 [4,7]
,[2,3]
需要额外箭 → 2支箭。两种排序方式结果相同,但按右端点排序的逻辑更统一,无需额外条件判断。
[[1,10], [2,3]]
10
处覆盖所有区间;按右端点排序后,射在 3
处即可覆盖所有区间。if (points.empty()) return 0; // 无气球需处理
使用 LONG_MIN
避免 INT_MIN
的溢出风险:
long last_end = LONG_MIN; // 初始化为极小值
for (int i = 0; i < points.size(); ++i) {
if (points[i][0] > last_end) { // 需要新箭
arrows++;
last_end = points[i][1]; // 贪心选择右端点
}
}
按右端点升序排序的方法在本题中体现以下优势:
这种策略通过贪心思想的局部最优选择,确保了全局最优解的必然性,是处理区间覆盖问题的经典范式。