LeetCode中等难度题目「452. 用最少数量的箭引爆气球」,这道题核心考察「贪心算法」的应用,属于区间问题的经典题型,学会这道题,能轻松应对同类区间合并/重叠问题。
先先明确题目要求,避免理解偏差——毕竟解题的第一步,是读懂题目本身。
一、题目解读(通俗版)
题目说,墙上贴了很多气球,每个气球用一个数组 [xstart, xend] 表示它的水平直径范围(y坐标不用管,因为弓箭是垂直射的)。
弓箭可以从x轴任意点垂直射出,射出后无限延伸,只要弓箭的x坐标落在某个气球的 [xstart, xend] 范围内,这个气球就会被引爆。
我们的目标是:用最少的弓箭,引爆所有气球,返回这个最少弓箭数。
二、核心解题思路(贪心思想)
这道题的关键的是:一支箭能引爆多个气球,只要这些气球的区间有重叠。所以本质上,我们要找的是「最多能覆盖多少个重叠气球」,每找到一组这样的重叠气球,就需要一支箭,最终箭的数量就是「不重叠的重叠区间组数」。
具体思路分两步:
-
排序:先将所有气球按「左端点xstart」从小到大排序。为什么按左端点?因为我们要从最左边的气球开始,依次判断后续气球是否能和当前气球重叠,尽可能让一支箭覆盖更多气球。
-
维护重叠区间:遍历排序后的气球,维护一个「当前重叠区间」(初始为第一个气球的区间)。对于每个后续气球:
-
如果当前气球的左端点 ≤ 当前重叠区间的右端点 → 两者有重叠,更新重叠区间的右端点为「当前重叠区间右端点」和「当前气球右端点」的最小值(关键!因为箭必须射在所有重叠气球的共同区间内,比如一个区间是[2,8],另一个是[1,6],它们的共同区间是[2,6],箭只能射在这个范围内才能同时引爆两个)。
-
如果当前气球的左端点 > 当前重叠区间的右端点 → 两者无重叠,需要新增一支箭,同时将当前重叠区间更新为这个新气球的区间。
-
最终,我们维护的「重叠区间的数量」,就是最少需要的弓箭数——因为每个重叠区间对应一支箭,刚好能引爆该区间内所有重叠的气球。
三、完整代码+逐行解析
先上完整可运行的TypeScript代码(兼容JavaScript,只需去掉类型注解即可):
function findMinArrowShots(points: number[][]): number {
// 处理边界情况:没有气球时,不需要任何箭
if (points.length === 0) return 0;
// 按Xstart从小到大排序(核心排序步骤)
const pointsSorted = points.sort((a, b) => a[0] - b[0]);
// 存储所有不重叠的重叠区间(每个区间对应一支箭)
const intersection: number[][] = [];
for (const point of pointsSorted) {
if (intersection.length === 0) {
// 第一个气球,直接作为第一个重叠区间(需要一支箭)
intersection.push(point);
} else {
// 取出最后一个有效的重叠区间
const lastIntersection = intersection[intersection.length - 1];
// 检查当前气球是否与最后一个重叠区间有重叠
if (point[0] <= lastIntersection[1]) {
// 有重叠:更新重叠区间的右端点为两者的较小值(关键操作)
// 保证更新后的区间是所有重叠气球的共同区间
lastIntersection[1] = Math.min(lastIntersection[1], point[1]);
} else {
// 无重叠:新增一支箭,将当前气球作为新的重叠区间
intersection.push(point);
}
}
}
// 重叠区间的数量 = 最少弓箭数
return intersection.length;
}
逐行解析(重点标注易错点)
-
if (points.length === 0) return 0;:边界处理,当没有气球时,直接返回0,避免后续遍历出错(易错点:忘记处理空数组)。 -
const pointsSorted = points.sort((a, b) => a[0] - b[0]);:按左端点xstart升序排序,排序是贪心的前提,确保我们从左到右依次处理气球(易错点:排序逻辑写反,比如写成b[0]-a[0],会导致后续判断出错)。 -
const intersection: number[][] = [];:用于存储「不重叠的重叠区间」,每个区间对应一支箭,数组长度就是最终答案。 -
if (intersection.length === 0) { intersection.push(point); }:遍历第一个气球时,因为没有之前的重叠区间,直接将其加入数组,代表需要第一支箭。 -
const lastIntersection = intersection[intersection.length - 1];:取出最后一个重叠区间,判断当前气球是否能和这个区间重叠。 -
if (point[0] <= lastIntersection[1]) { ... }:核心判断——当前气球左端点 ≤ 最后一个区间右端点,说明两者有重叠,此时不需要新增箭,只需更新重叠区间的右端点(易错点:这里容易写成point[1] >= lastIntersection[0],虽然逻辑上也能判断重叠,但不符合我们的贪心思路,且后续更新右端点会出错)。 -
lastIntersection[1] = Math.min(lastIntersection[1], point[1]);:最关键的一步!更新重叠区间的右端点为两者的最小值,确保这个区间是当前所有重叠气球的共同区间(比如:最后一个区间是[2,8],当前气球是[1,6],重叠区间更新为[2,6],箭射在2-6之间,就能同时引爆两个气球;如果不更新,还是[2,8],箭射在7-8之间,就无法引爆[1,6]这个气球)。 -
else { intersection.push(point); }:无重叠时,新增一支箭,将当前气球作为新的重叠区间。 -
return intersection.length;:重叠区间的数量就是最少弓箭数,直接返回即可。
四、易错点总结(避坑必看)
-
忘记处理空数组边界:当points为空时,直接返回0,否则会出现intersection.push(point)报错。
-
排序逻辑错误:必须按左端点升序排序(a[0]-b[0]),如果按右端点排序或降序排序,会导致重叠区间判断失误。
-
重叠区间更新错误:更新右端点时,必须取「当前重叠区间右端点」和「当前气球右端点」的最小值,而不是最大值——最大值会导致区间超出部分气球的范围,箭无法引爆所有重叠气球。
-
重叠判断逻辑写错:应该用「当前气球左端点 ≤ 最后一个区间右端点」,而不是「当前气球右端点 ≥ 最后一个区间左端点」,后者虽然能判断重叠,但不符合贪心思路,会导致后续区间维护混乱。
五、题目延伸(举一反三)
这道题属于「区间重叠」类贪心问题,同类经典题目还有:
-
LeetCode 56. 合并区间:思路类似,都是排序后维护区间,区别在于合并区间是取右端点最大值,而这道题是取最小值。
-
LeetCode 435. 无重叠区间:求最少需要移除的区间数量,核心也是排序+维护重叠区间,思路互通。
掌握这道题的贪心思路,这类区间问题就能迎刃而解——核心都是「排序+贪心选择局部最优(尽可能覆盖更多/保留更少),最终达到全局最优」。