用最少数量的箭引爆气球 图解题解
这道题到底在问什么
- 输入
- points=[[10,16],[2,8],[1,6],[7,12]]
- 输出
- 2 (一箭打 [1,6][2,8],一箭打 [7,12][10,16])
- 输入
- points=[[1,2],[3,4],[5,6],[7,8]]
- 输出
- 4 (四段互不重叠,各需一箭)
最优解:为什么这么做
一句话答案:LeetCode 452 用最少数量的箭引爆气球:把区间按右端升序排,一支箭钉在当前重叠组的最右端、左端越过箭位才新开一箭,时间 O(n log n)、空间 O(1)。
一支箭最少射几次,能引爆全部气球
给一组区间 points,每个 points[i]=[start, end] 是一个气球在水平方向占的一段范围。一支箭从某个 x 竖直射上去,凡是 start ≤ x ≤ end 的气球都会被引爆,要求箭最少。题面例子 points=[[10,16],[2,8],[1,6],[7,12]],答案 2:一箭穿过 [1,6] 和 [2,8],另一箭穿过 [7,12] 和 [10,16]。这等于把区间分成尽量少的几组,每组能被同一个 x 一起穿过。
挨个试哪些区间能共用一箭,会试到爆
枚举分组是第一反应:挨个试哪支箭罩哪几个区间、挑最少方案,可分法随区间数指数级涨,n 一大就配不完。换成先排序、再一趟扫过去边扫边定箭就快了——这是贪心:箭钉在当前重叠组的最右端就不再挪,后面区间自己往箭上靠、靠得着就连坐同爆。
按右端排序,箭钉在这一组的最右端
把区间按右端 end 从小到大排好,排完后第一个区间的右端,是全场最靠左的那条右边界——要引爆它,箭必须落在这条右边界之前。于是箭尽量往右挪,正好钉在它右端上:既打爆了它、又站到最靠右的落点,后面左端不超过箭位的区间全被顺带连坐、一起爆掉。
接着往后扫,每个区间只问一句:左端有没有越过当前箭的落点?没越过就和箭位有交集,白捡一个、箭不动;一旦左端超过箭位,这支箭最远只够到旧落点、再碰不到它,只能新开一箭钉在新区间右端。每次加箭都真省不掉,箭数便是最少。
加箭的判定,为什么卡在严格大于
加不加箭,卡在左端和箭位这一比:写成「左端 > 箭位就加箭」,只有左端严格超过箭位,两者才真没交集。若左端恰好等于箭位,左边界正压在箭上、端点也算被穿过,箭仍打得中它、不该多加;松成「左端 ≥ 箭位」,端点相接的区间会被错判成够不着,箭数凭空多出。另外坐标能到 2^31−1,箭位用 32 位整数存会溢出,得用 64 位 long。
题面两组数据,各要几支箭
先看 points=[[10,16],[2,8],[1,6],[7,12]],按右端排好是 [1,6]、[2,8]、[7,12]、[10,16]。第一箭钉在 [1,6] 右端,箭位 6,箭数 1。[2,8] 左端 2 > 6 吗?不,箭位 6 落在里头,顺带爆掉。[7,12] 左端 7 > 6,够不着了,新开一箭钉 12,箭数 2。[10,16] 左端 10 > 12 吗?不,被 12 连坐。扫完 2 支,正是题面给的 2。
再看 points=[[1,2],[3,4],[5,6],[7,8]],四段首尾都断开,排序后右端是 2、4、6、8。每一段的左端都 > 前一个箭位(3>2、5>4、7>6),支支都得新开一箭,谁也搭不上谁,一段一箭,答案 4。
复杂度与它的区间贪心近亲
耗时大头在排序,O(n log n);排完只是线性扫一遍 O(n),可忽略。额外只用箭位和箭数两个变量,空间 O(1)。这套「按右端排序 + 扫一遍」和无重叠区间(lc435)同源:那题问最多留几个互不重叠的区间,这题问最少几个点穿透所有区间,本质都是排完右端后数不相交的分组。
▶ 动画逐步走查(共 35 步)——想跟着动画一帧帧对照就展开
- 3为什么排右端、射右端?按右端排序后,第一个区间的右端是「最靠左的那条右边界」。要打中它,箭必须落在它的右端之前;而为了顺带多打几个,箭就尽量往右放——正好放在它的右端。等到某个区间的左端超过了当前箭位,说明它和这支箭再没有交集,只能开新箭。每次「不得不加箭」都是真省不掉的,所以箭数最少。
- 4例 1 开始。先把区间按右端从小到大排好序(上面每格的数字就是排序后各区间的右端 end,下标是排序序号)。第一支箭等会儿就射在最左这个区间的右端。
- 5处理排序后的第 1 个区间 [1,6]。直接在它的右端 x=6 射出第一支箭:既能打爆这个气球,又把箭放到了最靠右的位置,方便顺带打中后面左端 ≤ 6 的气球。
- 6第一支箭定在 x=6(绿色高亮的就是箭所在的右端格),箭数记为 1。接下来每个区间只问一句话:它的左端 start 有没有超过当前箭位 6?没超过就被这支箭顺手打了,超过了才要加箭。
- 7轮到区间 [2,8](蓝光标)。当前那支箭还停在 x=6(绿色格)。只看一件事:这个区间的左端 2 有没有超过箭位 6?2 没超过它。
- 8左端 2 没有超过当前箭位 6,说明现在这支箭(在 x=6)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 1。
- 9轮到区间 [7,12](蓝光标)。当前那支箭还停在 x=6(绿色格)。只看一件事:这个区间的左端 7 有没有超过箭位 6?7 比它大。
- 10左端 7 已经超过了旧箭位 6——那支箭最远只到 6,再也碰不到这个区间。只能新开一支箭,射在它自己的右端 x=12(绿色格跳到这里)。箭数加一,变成 2。
- 11轮到区间 [10,16](蓝光标)。当前那支箭还停在 x=12(绿色格)。只看一件事:这个区间的左端 10 有没有超过箭位 12?10 没超过它。
- 12左端 10 没有超过当前箭位 12,说明现在这支箭(在 x=12)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 2。
- 13例 1 扫完:绿色高亮的每一格都是一支箭的落点(射在某个区间的右端)。一共加了 2 次箭,所以最少需要 2 支箭就能打爆全部气球。
- 14例 2 开始。先把区间按右端从小到大排好序(上面每格的数字就是排序后各区间的右端 end,下标是排序序号)。第一支箭等会儿就射在最左这个区间的右端。
- 15处理排序后的第 1 个区间 [3,5]。直接在它的右端 x=5 射出第一支箭:既能打爆这个气球,又把箭放到了最靠右的位置,方便顺带打中后面左端 ≤ 5 的气球。
- 16第一支箭定在 x=5(绿色高亮的就是箭所在的右端格),箭数记为 1。接下来每个区间只问一句话:它的左端 start 有没有超过当前箭位 5?没超过就被这支箭顺手打了,超过了才要加箭。
- 17轮到区间 [1,6](蓝光标)。当前那支箭还停在 x=5(绿色格)。只看一件事:这个区间的左端 1 有没有超过箭位 5?1 没超过它。
- 18左端 1 没有超过当前箭位 5,说明现在这支箭(在 x=5)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 1。
- 19轮到区间 [2,8](蓝光标)。当前那支箭还停在 x=5(绿色格)。只看一件事:这个区间的左端 2 有没有超过箭位 5?2 没超过它。
- 20左端 2 没有超过当前箭位 5,说明现在这支箭(在 x=5)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 1。
- 21轮到区间 [7,12](蓝光标)。当前那支箭还停在 x=5(绿色格)。只看一件事:这个区间的左端 7 有没有超过箭位 5?7 比它大。
- 22左端 7 已经超过了旧箭位 5——那支箭最远只到 5,再也碰不到这个区间。只能新开一支箭,射在它自己的右端 x=12(绿色格跳到这里)。箭数加一,变成 2。
- 23轮到区间 [10,16](蓝光标)。当前那支箭还停在 x=12(绿色格)。只看一件事:这个区间的左端 10 有没有超过箭位 12?10 没超过它。
- 24左端 10 没有超过当前箭位 12,说明现在这支箭(在 x=12)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 2。
- 25轮到区间 [15,17](蓝光标)。当前那支箭还停在 x=12(绿色格)。只看一件事:这个区间的左端 15 有没有超过箭位 12?15 比它大。
- 26左端 15 已经超过了旧箭位 12——那支箭最远只到 12,再也碰不到这个区间。只能新开一支箭,射在它自己的右端 x=17(绿色格跳到这里)。箭数加一,变成 3。
- 27轮到区间 [14,18](蓝光标)。当前那支箭还停在 x=17(绿色格)。只看一件事:这个区间的左端 14 有没有超过箭位 17?14 没超过它。
- 28左端 14 没有超过当前箭位 17,说明现在这支箭(在 x=17)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 3。
- 29轮到区间 [20,23](蓝光标)。当前那支箭还停在 x=17(绿色格)。只看一件事:这个区间的左端 20 有没有超过箭位 17?20 比它大。
- 30左端 20 已经超过了旧箭位 17——那支箭最远只到 17,再也碰不到这个区间。只能新开一支箭,射在它自己的右端 x=23(绿色格跳到这里)。箭数加一,变成 4。
- 31轮到区间 [19,24](蓝光标)。当前那支箭还停在 x=23(绿色格)。只看一件事:这个区间的左端 19 有没有超过箭位 23?19 没超过它。
- 32左端 19 没有超过当前箭位 23,说明现在这支箭(在 x=23)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 4。
- 33轮到区间 [26,28](蓝光标)。当前那支箭还停在 x=23(绿色格)。只看一件事:这个区间的左端 26 有没有超过箭位 23?26 比它大。
- 34左端 26 已经超过了旧箭位 23——那支箭最远只到 23,再也碰不到这个区间。只能新开一支箭,射在它自己的右端 x=28(绿色格跳到这里)。箭数加一,变成 5。
- 35轮到区间 [25,30](蓝光标)。当前那支箭还停在 x=28(绿色格)。只看一件事:这个区间的左端 25 有没有超过箭位 28?25 没超过它。
- 36左端 25 没有超过当前箭位 28,说明现在这支箭(在 x=28)恰好落在这个区间里,气球已经被它一起打爆了。不用加箭,箭也不动(继续留在前面的右端,绿色格不变)。箭数还是 5。
- 37例 2 扫完:绿色高亮的每一格都是一支箭的落点(射在某个区间的右端)。一共加了 5 次箭,所以最少需要 5 支箭就能打爆全部气球。
⚠️ 容易写错的地方
✗ 错:按左端 start 排序
✓ 对:按右端 end 排序
贪心要把箭尽量放在「最靠左的右边界」上才能覆盖最多,按右端排序才对
✗ 错:判定写成 start >= arrow 就加箭
✓ 对:应是 start > arrow 才加箭
start == arrow 时区间恰好被箭穿过(端点也算覆盖),不该多加箭
✗ 错:用 int 存 arrow/右端
✓ 对:用 long 存
右端可能到 2^31-1,比较/相减会溢出,力扣这题就有这种大数据
完整代码(Python / C++ / Java)
Python
def findMinArrowShots(points):
if not points:
return 0
points.sort(key=lambda p: p[1]) # 按右端升序
arrows = 1
arrow = points[0][1] # 第一箭射在第一个区间的右端
for s, e in points[1:]:
if s > arrow: # 左端超过箭位,够不到
arrows += 1 # 新加一支箭
arrow = e # 射在当前区间的右端
return arrowsC++
int findMinArrowShots(vector<vector<int>>& points) {
if (points.empty()) return 0;
sort(points.begin(), points.end(),
[](auto& a, auto& b){ return a[1] < b[1]; }); // 按右端升序
int arrows = 1;
long arrow = points[0][1]; // 第一箭射在第一个区间右端
for (int i = 1; i < points.size(); i++) {
if (points[i][0] > arrow) { // 左端超过箭位
arrows++; // 新加一支箭
arrow = points[i][1]; // 射在当前区间右端
}
}
return arrows;
}Java
public int findMinArrowShots(int[][] points) {
if (points.length == 0) return 0;
Arrays.sort(points, (a, b) ->
Integer.compare(a[1], b[1])); // 按右端升序
int arrows = 1;
long arrow = points[0][1]; // 第一箭射在第一区间右端
for (int i = 1; i < points.length; i++) {
if (points[i][0] > arrow) { // 左端超过箭位
arrows++; // 新加一支箭
arrow = points[i][1]; // 射在当前区间右端
}
}
return arrows;
}复杂度
时间
O(n log n)
排序主导;排完后只扫一遍 O(n)
空间
O(1)~O(n)
只用 arrow/arrows 两个变量;排序所需栈空间视实现而定
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 用最少数量的箭引爆气球 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么按右端排序,不按左端?+
箭要尽量往右放才能顺带多打几个,而「能覆盖当前气球的最靠右落点」就是它的右端。按右端排序后,第一个区间的右端是全场最靠左的那条右边界,箭钉在这里既覆盖了它、又留在最右,能连坐后面所有左端不超过它的区间。按左端排序做不到这一点:左端小的区间右端可能拖得很长,箭钉在它右端会白白错过前面那些右端更短的气球,反而多费箭。
左端和箭位恰好相等时,到底加不加箭?+
不加。判定写成 start > arrow 才加箭,用的是严格大于。start 等于 arrow 时,区间的左边界正好压在箭的落点上,而题目说 start ≤ x ≤ end 端点也算被穿过,这支箭仍然打得中它,不必新开一支。若误写成 start ≥ arrow,端点相接的区间会被当成够不着而多加一箭,答案偏大。
它和「无重叠区间」是不是一类题?+
是的,都是经典的区间贪心,按右端排序后扫一遍。无重叠区间(lc435)问最多能保留几个互不重叠的区间,等价于最少删几个;这题问最少几个公共点能穿透所有区间。两者本质都是「按右端贪心、统计不相交的分组数」,一个数保留、一个数箭,扫描套路完全相通。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 用最少数量的箭引爆气球 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。