用最少箭爆气球:箭钉在最早结束的点
一支箭要同时打穿一串气球,必须落在它们的公共点上。当前这组里谁最早结束,就把箭射在那里;下一个气球左端已经越过这个点,只能新开一箭。
一支箭对应一组有公共点的气球。把箭钉在当前组最早结束的位置 end:新气球左端 ≤ end 就能一起爆,并可能把 end 再往左收;左端 > end 则必须新射一箭。按左端排序后维护这个 end,与按右端排序再逐个射击是同一贪心。时间 O(n log n)。
气球在数轴上是闭区间 [xstart, xend]。一支箭从整数 x 射出,所有包含 x 的气球一起爆掉。求打爆全部气球的最少箭数。
主例 [[10,16],[2,8],[1,6],[7,12]]。按左端排成 [1,6]、[2,8]、[7,12]、[10,16]。[1,6] 和 [2,8] 交在 [2,6],一箭射在 6 能同时打穿;[7,12] 从 7 才开始,盖不住 6,必须再射一箭,这一箭还能带上 [10,16]。答案是 2。
每个气球一箭肯定够,但不是最少。本文要回答:为什么箭应该钉在「当前这组最早结束」的位置,而不是区间正中或最右。
最早结束的点,是当前这组公共点的右边界
一支箭落在 x,它能打穿的气球必须都包含 x,也就是这些区间有公共交集。最少箭数等于:把气球分成尽可能少的组,每组内部存在公共点。任意选一个公共点都行;贪心要决定这个点取在哪,才能让当前这组尽量长。
把箭钉在当前组最早结束的位置。更早结束的气球再往右移一格就打不中它,所以这支箭不能比这个结束点更靠右。所有还能盖住这个结束点的气球,都可以搭这支箭;左端已经越过结束点的气球,怎样移动这支箭都救不了,只能新开一组。这就是按结束贪心:end 始终是「当前还能一起爆的气球里,最早的那个右端」。
代码按左端排序,从左往右扫,用一个变量 end 记住这个最早右端。新气球左端 ≤ end,它盖得住这支箭,并入当前组;若它自己结束得更早,end 收成它的右端——公共交集的右边界只能更左,不能更右。新气球左端 > end,与当前箭失之交臂,箭数加一,end 改成它的右端。按右端排序再「射在当前气球结束处、跳过所有被射中的」是同一句话,只是遍历顺序不同。
用手走主例。排序后先看 [1,6],第一支箭,end=6。下一个 [2,8]:2 ≤ 6,并入;8 不比 6 小,end 仍是 6,公共交集是 [2,6]。演示第一帧 cur 还标着出发时的 [1,6],注释写出并入后的交集 [2,6]。下一个 [7,12]:7 > 6,盖不住射在 6 的箭,新开一箭,end=12。演示第二帧。下一个 [10,16]:10 ≤ 12,并入;16 不比 12 小,end 仍是 12。两支箭打完。演示第三帧。
若看见 [2,8] 就急着把箭改射到 8,[1,6] 打不中,会多耗一箭。end 只准往左收、不准往右放,正是为了保住最早结束的那个气球。没有气球返回 0;只有一个气球返回 1。边界相切算重叠:左端等于 end 仍能打中,题目是闭区间。
end 只准变小公共交集的右端是组内 min(右端)。新来的气球结束得更早,箭必须跟着左移,否则最早结束的那个打不中。end 变大等于放弃已经入组的气球。
Go:贪心分组
func findMinArrowShots(points [][]int) int {sort.Slice(points, func(i, j int) bool {return points[i][0] < points[j][0]})arrows, end := 1, points[0][1]for i := 1; i < len(points); i++ {if points[i][0] <= end {if points[i][1] < end { end = points[i][1] }} else {arrows++; end = points[i][1]}}return arrows}
1按左端排序,从左往右决定每支箭能带多远。
2第一支箭先钉在第一只气球的结束处。
3左端还盖得住 end:并入,并在结束更早时把 end 左移。
4左端越过 end:新开一箭。主例 7>6,第二支箭钉在 12。
总结
箭钉在当前组最早结束的点;盖不住就新开一箭。主例两支,分别带走 [1,6][2,8] 和 [7,12][10,16]。
- 公共点的右边界是组内最早结束。end 只准变小。
- 左端 > end 才新开一箭。相切(左端等于 end)仍算打中。
- 按右端排序再射击,与按左端排序维护 end,是同一贪心。