[Medium] 452. Minimum Number of Arrows to Burst Balloons
There are some spherical balloons taped onto a flat wall that represents the XY-plane. The balloons are represented as a 2D array points where points[i] = [xstart, xend] denotes a balloon whose horizontal diameter stretches between xstart and xend. You do not know the exact y-coordinates of the balloons.
Arrows can be shot up directly vertically (in the positive y-direction) from different points along the x-axis. A balloon with xstart and xend is burst by an arrow shot at x if xstart <= x <= xend. There is no limit to the number of arrows that can be shot. A shot arrow keeps traveling up infinitely, bursting any balloons in its path.
Given the array points, return the minimum number of arrows that must be shot to burst all balloons.
Examples
Example 1:
Input: points = [[10,16],[2,8],[1,6],[7,12]]
Output: 2
Explanation: The balloons can be burst by 2 arrows:
- Shoot an arrow at x = 6, bursting the balloons [2,8] and [1,6].
- Shoot an arrow at x = 11, bursting the balloons [10,16] and [7,12].
Example 2:
Input: points = [[1,2],[3,4],[5,6],[7,8]]
Output: 4
Explanation: One arrow needs to be shot for each balloon.
Example 3:
Input: points = [[1,2],[2,3],[3,4],[4,5]]
Output: 2
Explanation: The balloons can be burst by 2 arrows:
- Shoot an arrow at x = 2, bursting the balloons [1,2] and [2,3].
- Shoot an arrow at x = 4, bursting the balloons [3,4] and [4,5].
Constraints
1 <= points.length <= 10^5points[i].length == 2-2^31 <= xstart < xend <= 2^31 - 1
Thinking Process
There are some spherical balloons taped onto a flat wall that represents the XY-plane. The balloons are represented as a 2D array points where points[i] = [xstart, xend] denotes a balloon whose horizontal diameter stretches between xstart and xend. You do not know the exact y-coordinates of the balloons.
Arrows can be shot up directly vertically (in the positive y-direction) from different points along the x-axis. A balloon with xstart and xend is burst by an arrow shot at x if xstart <= x <= xend. There is no limit to the number of arrows that can be shot. A shot arrow keeps traveling up infinitely, bursting any balloons in its path.
- Greedy works when local optimal choices lead to global optimum.
- Often sort first to make the greedy choice obvious.
- Prove or sanity-check: would swapping two choices ever help?
Common Approaches
Typical techniques for this pattern:
| Approach | Time | Space | Notes |
|---|---|---|---|
| Sort + greedy (this problem) | O(n log n) | O(1) | Interval scheduling, assignment |
| Local greedy choice | O(n) | O(1) | Jump game, gas station |
| Greedy + heap | O(n log n) | O(n) | Merge streams, room allocation |
| Exchange argument | O(n) | O(1) | Prove greedy choice is safe |
Solution
Solution: Greedy with End Coordinate Sorting
class Solution:
def findMinArrowShots(self, points):
if len(points) == 0:
return 0
points.sort(key=lambda x: x[1])
pos = points[0][1]
arrows = 1
for balloon in points:
if balloon[0] > pos:
pos = balloon[1]
arrows += 1
return arrows
Solution Explanation
Approach: Sort + greedy (this problem)
Key idea: There are some spherical balloons taped onto a flat wall that represents the XY-plane. The balloons are represented as a 2D array points where points[i] = [xstart, xend] denotes a balloon whose horizontal diameter stretches between xstart and xend. You do not know the exact y-coordinates of the balloons.
How the code works:
- Greedy works when local optimal choices lead to global optimum.
- Often sort first to make the greedy choice obvious.
- Prove or sanity-check: would swapping two choices ever help?
Walkthrough — input points = [[10,16],[2,8],[1,6],[7,12]], expected output 2:
The balloons can be burst by 2 arrows:
- Shoot an arrow at x = 6, bursting the balloons [2,8] and [1,6].
- Shoot an arrow at x = 11, bursting the balloons [10,16] and [7,12].
Time: - Sorting: O(n log n) where n is the number of balloons · Space: O(1)
Algorithm Explanation:
- Edge Case (Line 4):
- If points array is empty, return 0 (no arrows needed)
- Sort by End Coordinate (Lines 5-7):
- Sort balloons by their end coordinate (
u[1] < v[1]) - This ensures we process balloons with earlier end coordinates first
- Key: Sorting by end coordinate allows greedy choice to be optimal
- Sort balloons by their end coordinate (
- Initialize (Lines 8-9):
pos: Position of the last arrow shot (end coordinate of first balloon)arrows: Number of arrows needed (start with 1 for the first balloon)
- Process Remaining Balloons (Lines 10-14):
- For each balloon starting from index 1:
- If doesn’t overlap:
ballon[0] > pos- Current balloon’s start is after last arrow position
- Shoot new arrow at this balloon’s end coordinate
- Update
posto current balloon’s end - Increment
arrows
- Else: Balloon overlaps with current arrow position
- Current arrow can burst this balloon (no new arrow needed)
- Don’t update
pos(keep shooting at same position)
- If doesn’t overlap:
- For each balloon starting from index 1:
Example Walkthrough:
Example 1: points = [[10,16],[2,8],[1,6],[7,12]]
After sorting by end coordinate:
Original: [[10,16],[2,8],[1,6],[7,12]]
Sorted: [[1,6], [2,8], [7,12], [10,16]]
[1,6] end=6
[2,8] end=8
[7,12] end=12
[10,16] end=16
Execution:
Initial: pos = 6 (from [1,6]), arrows = 1
i=1: [2,8]
Check: 2 > 6? No (overlaps, arrow at x=6 can burst it)
No new arrow needed
i=2: [7,12]
Check: 7 > 6? Yes (doesn't overlap)
Shoot new arrow: pos = 12, arrows = 2
i=3: [10,16]
Check: 10 > 12? No (overlaps, arrow at x=12 can burst it)
No new arrow needed
Result: 2 arrows
Arrow positions: x=6 (bursts [1,6], [2,8]), x=12 (bursts [7,12], [10,16])
Example 2: points = [[1,2],[3,4],[5,6],[7,8]]
After sorting by end coordinate:
Already sorted: [[1,2], [3,4], [5,6], [7,8]]
Execution:
Initial: pos = 2 (from [1,2]), arrows = 1
i=1: [3,4]
Check: 3 > 2? Yes (doesn't overlap)
Shoot new arrow: pos = 4, arrows = 2
i=2: [5,6]
Check: 5 > 4? Yes (doesn't overlap)
Shoot new arrow: pos = 6, arrows = 3
i=3: [7,8]
Check: 7 > 6? Yes (doesn't overlap)
Shoot new arrow: pos = 8, arrows = 4
Result: 4 arrows (one for each balloon)
Example 3: points = [[1,2],[2,3],[3,4],[4,5]]
After sorting by end coordinate:
Already sorted: [[1,2], [2,3], [3,4], [4,5]]
Execution:
Initial: pos = 2 (from [1,2]), arrows = 1
i=1: [2,3]
Check: 2 > 2? No (overlaps, arrow at x=2 can burst it)
No new arrow needed
i=2: [3,4]
Check: 3 > 2? Yes (doesn't overlap)
Shoot new arrow: pos = 4, arrows = 2
i=3: [4,5]
Check: 4 > 4? No (overlaps, arrow at x=4 can burst it)
No new arrow needed
Result: 2 arrows
Arrow positions: x=2 (bursts [1,2], [2,3]), x=4 (bursts [3,4], [4,5])
Algorithm Breakdown
Why Sort by End Coordinate?
Sorting by end coordinate is crucial for the greedy strategy:
- Maximize Coverage: Shooting at the end of a balloon maximizes the number of overlapping balloons we can burst
- Optimal Choice: This choice is always optimal (doesn’t prevent optimal solutions later)
- Greedy Property: Shooting at the earliest end coordinate leaves maximum room for future balloons
Counter-example (sorting by start coordinate):
Balloons: [[1,10], [2,3], [4,5]]
Sort by start: [[1,10], [2,3], [4,5]]
Shoot at x=10 → bursts [1,10]
[2,3] overlaps (2 <= 10) → already burst
[4,5] overlaps (4 <= 10) → already burst
Arrows: 1
Sort by end: [[2,3], [4,5], [1,10]]
Shoot at x=3 → bursts [2,3]
[4,5] doesn't overlap (4 > 3) → shoot at x=5
[1,10] overlaps (1 <= 5) → already burst
Arrows: 2
Wait, this doesn't show the issue. Let me think of a better example...
Actually, sorting by end is correct. The issue with sorting by start would be:
[[1,6], [2,3], [4,5]]
Sort by start: [[1,6], [2,3], [4,5]]
Shoot at x=6 → bursts all (optimal, 1 arrow)
Sort by end: [[2,3], [4,5], [1,6]]
Shoot at x=3 → bursts [2,3]
[4,5] doesn't overlap (4 > 3) → shoot at x=5
[1,6] overlaps (1 <= 5) → already burst
Arrows: 2 (suboptimal!)
Hmm, but wait - if we shoot at x=6 in the sorted-by-end approach, we'd get:
Shoot at x=6 → bursts [2,3], [1,6]
[4,5] overlaps (4 <= 6) → already burst
Arrows: 1 (optimal!)
The key is that we shoot at the END of the first balloon, which is x=3, not x=6. But [1,6] starts at 1, which is <= 3, so it should be burst. But [1,6] ends at 6, which is after 3, so if we shoot at x=3, we can't burst [1,6] because 3 is not in [1,6]... wait, 3 IS in [1,6] (1 <= 3 <= 6), so it should work.
Actually, I think the algorithm is correct. Let me verify with the actual execution:
- After sorting by end: [[2,3], [4,5], [1,6]]
- pos = 3, arrows = 1
- [4,5]: 4 > 3? Yes → new arrow at x=5, arrows=2
- [1,6]: 1 > 5? No → overlaps, already burst by arrow at x=5
But wait, arrow at x=5 cannot burst [1,6] because 5 is in [1,6]? Yes, 1 <= 5 <= 6, so it can burst it. So the algorithm is correct.
The key insight is that we shoot at the END of balloons, and a balloon [a,b] can be burst by an arrow at x if a <= x <= b. So if we shoot at x=5, it can burst [1,6] because 1 <= 5 <= 6.
Overlap Detection
Two balloons [a, b] and [c, d] can be burst by the same arrow if they overlap:
They overlap if: c <= b (current start <= previous end)
Why > not >=?
- We check
ballon[0] > posto see if we need a new arrow - If
ballon[0] <= pos, the balloon overlaps with the current arrow position - The arrow at
poscan burst this balloon (sinceballon[0] <= pos <= ballon[1])
Greedy Strategy
The algorithm uses a greedy strategy:
- Sort by end coordinate: Process balloons with earlier end coordinates first
- Shoot at end: Always shoot arrow at the end coordinate of the first balloon in a group
- Maximize coverage: This maximizes the number of overlapping balloons burst by one arrow
This minimizes the number of arrows needed.
Time & Space Complexity
- Time Complexity:
- Sorting: O(n log n) where n is the number of balloons
- Iteration: O(n) - single pass through sorted balloons
- Total: O(n log n)
- Space Complexity: O(1)
- Only using a few variables
- Sorting is in-place (or O(n) if not in-place, but typically O(1) extra space)
Key Points
- Sort by End Coordinate: Critical for greedy strategy to work
- Greedy Choice: Shoot arrow at end of first balloon in each group
- Overlap Check: Use
>to check if new arrow needed - Boundary Touch: Balloons touching at boundary can be burst by same arrow
- Optimal: Greedy approach finds optimal solution
Common Mistakes
- Empty array:
[]→ return 0 - Single balloon:
[[1,2]]→ return 1 - No overlaps:
[[1,2],[3,4],[5,6]]→ return 3 - All overlap:
[[1,5],[2,6],[3,7]]→ return 1 - Boundary touch:
[[1,2],[2,3],[3,4]]→ return 2 -
Nested balloons:
[[1,10],[2,3],[4,5]]→ return 1 - Wrong sort key: Sorting by start coordinate instead of end coordinate
- Wrong overlap condition: Using
>=instead of> - Not handling empty input: Forgetting edge case
- Wrong initialization: Not initializing
posandarrowscorrectly - Off-by-one error: Starting loop from wrong index
Related Problems
- 435. Non-overlapping Intervals - Remove minimum intervals (similar greedy)
- 56. Merge Intervals - Merge overlapping intervals
- 253. Meeting Rooms II - Count overlapping intervals
- 646. Maximum Length of Pair Chain - Find longest chain
Tags
Array, Greedy, Sorting, Intervals, Medium
Key Takeaways
- Greedy works when local optimal choices lead to global optimum.
- Often sort first to make the greedy choice obvious.
- Prove or sanity-check: would swapping two choices ever help?
References
- LC 452: Minimum Number of Arrows to Burst Balloons on LeetCode
- LeetCode Discuss — LC 452: Minimum Number of Arrows to Burst Balloons
- LeetCode Editorial (may require premium)