Builds on Interval Scheduling, which you haven’t finished yet.
Interval Grouping (Minimum Arrows to Burst Balloons)
Each balloon is a horizontal span [xstart, xend] on the x-axis, and points lists one span per balloon. The height of a balloon is not given.
An arrow flies straight up from a point x on the x-axis. It bursts every balloon whose span contains x, which means xstart <= x <= xend. You can shoot as many arrows as you like, and an arrow never stops. A balloon whose span only touches x with its edge is also burst.
Return the fewest arrows that burst every balloon.
[1,6], [2,8], [7,12] and [10,16]. An arrow at 6 bursts [1,6] and [2,8]. The balloon [7,12] starts after 6, so a second arrow goes at 12 and bursts [7,12] and [10,16].End-Time Groups
intervals ➔ sort by end ➔ one arrow at the end of each interval not yet hit| 1 | def find_min_arrow_shots(points: list[list[int]]) -> int: |
| 2 | """Minimum Number of Arrows to Burst Balloons: sort by end, shoot at the end of every balloon not yet hit. |
| 3 | Time: O(N log N) | Space: O(1) besides the sort |
| 4 | """ |
| 5 | # 1. Sort By End Coordinate: |
| 6 | # The balloon that ends first is the safest one to shoot: an arrow at its end hits it, and hits every later |
| 7 | # balloon that has already started. |
| 8 | points.sort(key=lambda p: p[1]) |
| 9 | |
| 10 | arrows = 0 |
| 11 | arrow_x = None # x position of the last arrow; None before the first shot |
| 12 | |
| 13 | for start, end in points: |
| 14 | # 2. Shoot Only When The Balloon Is Not Already Hit: |
| 15 | # The last arrow hits this balloon when the balloon starts at or before arrow_x. A balloon that only touches |
| 16 | # arrow_x still counts as hit, so the test is a strict start > arrow_x. |
| 17 | if arrow_x is None or start > arrow_x: |
| 18 | arrows += 1 |
| 19 | arrow_x = end # Shoot at this balloon's end: as far right as it can go and still hit it |
| 20 | |
| 21 | return arrows |
arrow_x is the end of the last interval that needed a new arrow, so it is the rightmost spot where that arrow still hits it.Sort the intervals by end and keep arrow_x, the spot of the last arrow. An interval that starts at or before arrow_x is already hit, because every earlier interval ends no later than it does. An interval that starts after arrow_x needs a new arrow, and the best spot for it is its own end, as far right as it can go and still hit it. The trap is the comparison: a balloon that only touches arrow_x is hit, so the test is start > arrow_x, not >=. This is the opposite choice from Interval Scheduling, where touching intervals do not overlap.
See it run
End-Time Groups, step by step
arrow_x 3arrows 1
Each column is one x position. Each row of numbers is one balloon, sorted by end. A dash marks the x positions the balloon covers. The word shot or hit replaces the dashes after the decision.
startthe start of the balloon being checkedendthe end of the balloon being checkedarrow_xthe x position of the last arrow
Sort by end. The first balloon [2, 3] is not hit yet, so the first arrow goes to its end: arrow_x = 3.
Gotcha 1 of 3
- The trap
Gotcha 1: Treating a balloon that only touches the arrow as not hit
Mistakeif arrow_x is None or start >= arrow_x:Fixif arrow_x is None or start > arrow_x:What happens: A balloon
[2, 3]and an arrow at2share the point2, so the arrow hits it. With>=the code counts a new arrow for it, so every pair of balloons that only touch costs an extra arrow.Example: points = [[1, 2], [2, 3], [3, 4], [4, 5]]Mistake: returns 4Fix: returns 2
- Non-overlapping Intervals (LC 435) counts the intervals to remove, and intervals that only touch do not overlap. Here a shared point is a hit, so touching intervals share one arrow. See Interval Scheduling
Deep Dive
What this card adds to Interval Scheduling
This card builds on the Interval Scheduling card. That card sorts by end and counts the intervals to remove. This card sorts by end too, but it counts groups. A group is a set of intervals that all contain one common point.
In Minimum Number of Arrows to Burst Balloons (LC 452), each balloon is a range [start, end] on the x axis. One arrow at position x bursts every balloon with start <= x <= end. The question asks for the fewest arrows, so it asks for the fewest groups.
Both cards use the same sort and the same loop. Two things change. The loop does not remove anything: it shoots a new arrow instead. And the touch rule is reversed. In Interval Scheduling, two intervals that only touch do not overlap. Here, a balloon that only touches the arrow is hit.
Why the arrow goes to the end of the first balloon not yet hit
Look at the balloon that ends first. Some arrow must hit it, so that arrow sits at a position x with start <= x <= end for this balloon. Move the arrow right, up to this balloon's end. It still hits this balloon. It now hits every balloon that the old arrow hit, because every other balloon ends at or after this end, and it already contains the old position x. So an arrow at end is never worse.
After that arrow, a balloon is hit when start <= arrow_x. Since the list is sorted by end, every balloon that comes later ends at or after arrow_x. The only question is where it starts.
The loop does this with arrow_x. A balloon with start > arrow_x starts after the arrow, so no earlier arrow reaches it. The loop adds 1 to arrows and sets arrow_x = end. Every other balloon is already hit, so nothing changes. The starting value is None, so the first balloon always gets an arrow.
Example: in LC 452 with points = [[10, 16], [2, 8], [1, 6], [7, 12]], the sorted order is [1, 6], [2, 8], [7, 12], [10, 16]. The first arrow goes to 6 and hits [1, 6] and [2, 8]. [7, 12] starts at 7, which is greater than 6, so the second arrow goes to 12. It hits [7, 12] and [10, 16]. The answer is 2.
One trace: a nested balloon and a touching balloon
The board replays points = [[1, 6], [2, 3], [4, 5], [5, 8], [7, 9]]. The rows are in sorted order by end, so the first row is [2, 3].
The first arrow goes to 3 and bursts [2, 3]. [4, 5] starts at 4, which is greater than 3, so a second arrow goes to 5. [1, 6] contains [4, 5]. It starts at 1, which is not greater than 5, so the same arrow hits it.
[5, 8] starts at 5, and 5 > 5 is false. The balloon only touches the arrow at 5, and the arrow still hits it. This is the step marked as the trap on the board. Then [7, 9] starts at 7, which is greater than 5, so a third arrow goes to 9. The answer is 3.
The test start >= arrow_x would shoot a new arrow for [5, 8]. For the four balloons [[1, 2], [2, 3], [3, 4], [4, 5]] the code would then return 4 instead of 2, because every pair of neighbours only touches.
The trap: sorting by start, and the cost
Keep the same balloons and sort by start. The order is [1, 6], [2, 3], [4, 5], [5, 8], [7, 9]. The first arrow goes to 6, the end of the long balloon [1, 6]. The code then treats [2, 3] as hit, but 6 is not inside [2, 3]. The code returns 2 instead of 3.
The reason is that the end of the first balloon by start can lie to the right of the short balloons inside it. With the sort by end, arrow_x is always the smallest end among the balloons that are not hit yet, so every later balloon that starts at or before it really contains it.
The sort takes O(N log N) time and the loop takes O(N) time. The loop keeps only arrow_x and arrows, so the extra space is O(1) besides the sort. Start arrow_x at None and not at 0, because coordinates can be negative.
The same idea solves Maximum Length of Pair Chain (LC 646), but there the touch rule is the opposite: a pair can follow another only when its first number is strictly greater than the previous second number. Check the rule of each question before you choose between > and >=.
60-Second Pitch
"One arrow bursts all balloons that contain its position, so I want the fewest groups of balloons that share a point. I sort by end. The balloon that ends first must be hit by some arrow, and I can move that arrow right to the balloon's end without losing any balloon. I keep arrow_x, the position of the last arrow. For each balloon, if start > arrow_x I shoot a new arrow, add 1 to arrows, and set arrow_x = end. Otherwise the last arrow already hits it. The sort costs O(N log N) and the loop costs O(N), with O(1) extra space. I check that a balloon that only touches the arrow is hit, so the test is > and not >=."
Common Questions
- Why is the test
start > arrow_xand not>=? The ranges in Minimum Number of Arrows to Burst Balloons (LC 452) include their end points. A balloon
[5, 8]and an arrow at 5 share the position 5, so the arrow bursts it. With>=, the code shoots an extra arrow for every balloon that only touches the arrow.- Why sort by end and not by start?
The arrow goes to the end of a balloon. After the sort by end, that end is the smallest end among the balloons not yet hit, so every later balloon that starts at or before it contains it. After a sort by start, the first balloon can be long, and its end can lie to the right of a short balloon inside it.
- How is this different from Non-overlapping Intervals (LC 435)?
Both sort by end and use
endas the marker. Non-overlapping Intervals (LC 435) counts the intervals to remove, and intervals that only touch do not overlap, so its test isstart < prev_end. This card counts the groups, and a touch is a hit, so its test isstart > arrow_x.- What are the time and space complexity?
O(N log N)time, because of the sort. The loop isO(N). The extra space isO(1)besides the space the sort uses. The blueprint changes the caller's list by sorting it.- Why start
arrow_xatNoneand not at 0? Coordinates can be negative. With
arrow_x = 0, a balloon such as[-5, -1]hasstart > arrow_xfalse, so it looks hit and the first arrow is never shot.Nonemakes the first balloon always get an arrow.
SWE Reasoning
Spot it
- The question asks for the fewest points that touch every interval.
- Or it asks to split the intervals into the fewest groups, where every group shares a common point.
- A shared end point counts as a shared point, so touching intervals can go into one group.
- The answer is a count, and the positions of the points are not needed.
- The intervals are balloons, meetings that need one shared moment, or ranges that one action can cover.
The invariant, step by step
- Sort
pointsbyp[1], which is the end. Setarrows = 0andarrow_x = None. - For each
start, endin the sorted list, testarrow_x is None or start > arrow_x. - If it is true, no earlier arrow reaches this balloon. Shoot a new arrow:
arrows += 1. - Set
arrow_x = end. This is the rightmost position that still hits this balloon. - If the test is false, the balloon starts at or before
arrow_x, and it ends at or afterarrow_x. The last arrow hits it, and the loop changes nothing. - Every balloon before the current one is hit by one of the arrows. Return
arrows.
Traps
- Using
>=in the test. A balloon that only touchesarrow_xis hit. For[[1, 2], [2, 3], [3, 4], [4, 5]]the code returns 4 instead of 2. - Sorting by start. The end of a long balloon can lie to the right of the short balloons inside it. For
[[1, 6], [2, 3], [4, 5]]the code returns 1 instead of 2. - Starting
arrow_xat 0. Coordinates can be negative. For[[-5, -1]]the code returns 0 instead of 1. UseNone. - Moving
arrow_xwhen a balloon is hit. Only a new arrow setsarrow_x. A hit balloon must leave it unchanged.
Practice
Builds on Interval Scheduling, which you haven’t finished yet.
Interval Grouping (Minimum Arrows to Burst Balloons)
Each balloon is a horizontal span [xstart, xend] on the x-axis, and points lists one span per balloon. The height of a balloon is not given.
An arrow flies straight up from a point x on the x-axis. It bursts every balloon whose span contains x, which means xstart <= x <= xend. You can shoot as many arrows as you like, and an arrow never stops. A balloon whose span only touches x with its edge is also burst.
Return the fewest arrows that burst every balloon.
[1,6], [2,8], [7,12] and [10,16]. An arrow at 6 bursts [1,6] and [2,8]. The balloon [7,12] starts after 6, so a second arrow goes at 12 and bursts [7,12] and [10,16].End-Time Groups
intervals ➔ sort by end ➔ one arrow at the end of each interval not yet hit| 1 | def find_min_arrow_shots(points: list[list[int]]) -> int: |
| 2 | """Minimum Number of Arrows to Burst Balloons: sort by end, shoot at the end of every balloon not yet hit. |
| 3 | Time: O(N log N) | Space: O(1) besides the sort |
| 4 | """ |
| 5 | # 1. Sort By End Coordinate: |
| 6 | # The balloon that ends first is the safest one to shoot: an arrow at its end hits it, and hits every later |
| 7 | # balloon that has already started. |
| 8 | points.sort(key=lambda p: p[1]) |
| 9 | |
| 10 | arrows = 0 |
| 11 | arrow_x = None # x position of the last arrow; None before the first shot |
| 12 | |
| 13 | for start, end in points: |
| 14 | # 2. Shoot Only When The Balloon Is Not Already Hit: |
| 15 | # The last arrow hits this balloon when the balloon starts at or before arrow_x. A balloon that only touches |
| 16 | # arrow_x still counts as hit, so the test is a strict start > arrow_x. |
| 17 | if arrow_x is None or start > arrow_x: |
| 18 | arrows += 1 |
| 19 | arrow_x = end # Shoot at this balloon's end: as far right as it can go and still hit it |
| 20 | |
| 21 | return arrows |
arrow_x is the end of the last interval that needed a new arrow, so it is the rightmost spot where that arrow still hits it.Sort the intervals by end and keep arrow_x, the spot of the last arrow. An interval that starts at or before arrow_x is already hit, because every earlier interval ends no later than it does. An interval that starts after arrow_x needs a new arrow, and the best spot for it is its own end, as far right as it can go and still hit it. The trap is the comparison: a balloon that only touches arrow_x is hit, so the test is start > arrow_x, not >=. This is the opposite choice from Interval Scheduling, where touching intervals do not overlap.
See it run
End-Time Groups, step by step
arrow_x 3arrows 1
Each column is one x position. Each row of numbers is one balloon, sorted by end. A dash marks the x positions the balloon covers. The word shot or hit replaces the dashes after the decision.
startthe start of the balloon being checkedendthe end of the balloon being checkedarrow_xthe x position of the last arrow
Sort by end. The first balloon [2, 3] is not hit yet, so the first arrow goes to its end: arrow_x = 3.
Gotcha 1 of 3
- The trap
Gotcha 1: Treating a balloon that only touches the arrow as not hit
Mistakeif arrow_x is None or start >= arrow_x:Fixif arrow_x is None or start > arrow_x:What happens: A balloon
[2, 3]and an arrow at2share the point2, so the arrow hits it. With>=the code counts a new arrow for it, so every pair of balloons that only touch costs an extra arrow.Example: points = [[1, 2], [2, 3], [3, 4], [4, 5]]Mistake: returns 4Fix: returns 2
- Non-overlapping Intervals (LC 435) counts the intervals to remove, and intervals that only touch do not overlap. Here a shared point is a hit, so touching intervals share one arrow. See Interval Scheduling
Deep Dive
What this card adds to Interval Scheduling
This card builds on the Interval Scheduling card. That card sorts by end and counts the intervals to remove. This card sorts by end too, but it counts groups. A group is a set of intervals that all contain one common point.
In Minimum Number of Arrows to Burst Balloons (LC 452), each balloon is a range [start, end] on the x axis. One arrow at position x bursts every balloon with start <= x <= end. The question asks for the fewest arrows, so it asks for the fewest groups.
Both cards use the same sort and the same loop. Two things change. The loop does not remove anything: it shoots a new arrow instead. And the touch rule is reversed. In Interval Scheduling, two intervals that only touch do not overlap. Here, a balloon that only touches the arrow is hit.
Why the arrow goes to the end of the first balloon not yet hit
Look at the balloon that ends first. Some arrow must hit it, so that arrow sits at a position x with start <= x <= end for this balloon. Move the arrow right, up to this balloon's end. It still hits this balloon. It now hits every balloon that the old arrow hit, because every other balloon ends at or after this end, and it already contains the old position x. So an arrow at end is never worse.
After that arrow, a balloon is hit when start <= arrow_x. Since the list is sorted by end, every balloon that comes later ends at or after arrow_x. The only question is where it starts.
The loop does this with arrow_x. A balloon with start > arrow_x starts after the arrow, so no earlier arrow reaches it. The loop adds 1 to arrows and sets arrow_x = end. Every other balloon is already hit, so nothing changes. The starting value is None, so the first balloon always gets an arrow.
Example: in LC 452 with points = [[10, 16], [2, 8], [1, 6], [7, 12]], the sorted order is [1, 6], [2, 8], [7, 12], [10, 16]. The first arrow goes to 6 and hits [1, 6] and [2, 8]. [7, 12] starts at 7, which is greater than 6, so the second arrow goes to 12. It hits [7, 12] and [10, 16]. The answer is 2.
One trace: a nested balloon and a touching balloon
The board replays points = [[1, 6], [2, 3], [4, 5], [5, 8], [7, 9]]. The rows are in sorted order by end, so the first row is [2, 3].
The first arrow goes to 3 and bursts [2, 3]. [4, 5] starts at 4, which is greater than 3, so a second arrow goes to 5. [1, 6] contains [4, 5]. It starts at 1, which is not greater than 5, so the same arrow hits it.
[5, 8] starts at 5, and 5 > 5 is false. The balloon only touches the arrow at 5, and the arrow still hits it. This is the step marked as the trap on the board. Then [7, 9] starts at 7, which is greater than 5, so a third arrow goes to 9. The answer is 3.
The test start >= arrow_x would shoot a new arrow for [5, 8]. For the four balloons [[1, 2], [2, 3], [3, 4], [4, 5]] the code would then return 4 instead of 2, because every pair of neighbours only touches.
The trap: sorting by start, and the cost
Keep the same balloons and sort by start. The order is [1, 6], [2, 3], [4, 5], [5, 8], [7, 9]. The first arrow goes to 6, the end of the long balloon [1, 6]. The code then treats [2, 3] as hit, but 6 is not inside [2, 3]. The code returns 2 instead of 3.
The reason is that the end of the first balloon by start can lie to the right of the short balloons inside it. With the sort by end, arrow_x is always the smallest end among the balloons that are not hit yet, so every later balloon that starts at or before it really contains it.
The sort takes O(N log N) time and the loop takes O(N) time. The loop keeps only arrow_x and arrows, so the extra space is O(1) besides the sort. Start arrow_x at None and not at 0, because coordinates can be negative.
The same idea solves Maximum Length of Pair Chain (LC 646), but there the touch rule is the opposite: a pair can follow another only when its first number is strictly greater than the previous second number. Check the rule of each question before you choose between > and >=.
60-Second Pitch
"One arrow bursts all balloons that contain its position, so I want the fewest groups of balloons that share a point. I sort by end. The balloon that ends first must be hit by some arrow, and I can move that arrow right to the balloon's end without losing any balloon. I keep arrow_x, the position of the last arrow. For each balloon, if start > arrow_x I shoot a new arrow, add 1 to arrows, and set arrow_x = end. Otherwise the last arrow already hits it. The sort costs O(N log N) and the loop costs O(N), with O(1) extra space. I check that a balloon that only touches the arrow is hit, so the test is > and not >=."
Common Questions
- Why is the test
start > arrow_xand not>=? The ranges in Minimum Number of Arrows to Burst Balloons (LC 452) include their end points. A balloon
[5, 8]and an arrow at 5 share the position 5, so the arrow bursts it. With>=, the code shoots an extra arrow for every balloon that only touches the arrow.- Why sort by end and not by start?
The arrow goes to the end of a balloon. After the sort by end, that end is the smallest end among the balloons not yet hit, so every later balloon that starts at or before it contains it. After a sort by start, the first balloon can be long, and its end can lie to the right of a short balloon inside it.
- How is this different from Non-overlapping Intervals (LC 435)?
Both sort by end and use
endas the marker. Non-overlapping Intervals (LC 435) counts the intervals to remove, and intervals that only touch do not overlap, so its test isstart < prev_end. This card counts the groups, and a touch is a hit, so its test isstart > arrow_x.- What are the time and space complexity?
O(N log N)time, because of the sort. The loop isO(N). The extra space isO(1)besides the space the sort uses. The blueprint changes the caller's list by sorting it.- Why start
arrow_xatNoneand not at 0? Coordinates can be negative. With
arrow_x = 0, a balloon such as[-5, -1]hasstart > arrow_xfalse, so it looks hit and the first arrow is never shot.Nonemakes the first balloon always get an arrow.
SWE Reasoning
Spot it
- The question asks for the fewest points that touch every interval.
- Or it asks to split the intervals into the fewest groups, where every group shares a common point.
- A shared end point counts as a shared point, so touching intervals can go into one group.
- The answer is a count, and the positions of the points are not needed.
- The intervals are balloons, meetings that need one shared moment, or ranges that one action can cover.
The invariant, step by step
- Sort
pointsbyp[1], which is the end. Setarrows = 0andarrow_x = None. - For each
start, endin the sorted list, testarrow_x is None or start > arrow_x. - If it is true, no earlier arrow reaches this balloon. Shoot a new arrow:
arrows += 1. - Set
arrow_x = end. This is the rightmost position that still hits this balloon. - If the test is false, the balloon starts at or before
arrow_x, and it ends at or afterarrow_x. The last arrow hits it, and the loop changes nothing. - Every balloon before the current one is hit by one of the arrows. Return
arrows.
Traps
- Using
>=in the test. A balloon that only touchesarrow_xis hit. For[[1, 2], [2, 3], [3, 4], [4, 5]]the code returns 4 instead of 2. - Sorting by start. The end of a long balloon can lie to the right of the short balloons inside it. For
[[1, 6], [2, 3], [4, 5]]the code returns 1 instead of 2. - Starting
arrow_xat 0. Coordinates can be negative. For[[-5, -1]]the code returns 0 instead of 1. UseNone. - Moving
arrow_xwhen a balloon is hit. Only a new arrow setsarrow_x. A hit balloon must leave it unchanged.