452. Minimum Number of Arrows to Burst Balloons
Problem Description
You have balloons attached to a wall represented as a 2D plane. Each balloon is given as an interval [x_start, x_end]
representing its horizontal span along the x-axis. The balloons extend vertically (we don't know their exact y-coordinates), but we know their horizontal coverage.
You can shoot arrows vertically upward from any position along the x-axis. When an arrow is shot from position x
, it travels straight up infinitely and bursts any balloon whose horizontal range includes x
. In other words, a balloon with range [x_start, x_end]
gets burst if the arrow is shot from any position x
where x_start โค x โค x_end
.
The goal is to find the minimum number of arrows needed to burst all balloons.
Key Points:
- Each balloon is represented as
[x_start, x_end]
- An arrow shot at position
x
bursts all balloons wherex_start โค x โค x_end
- Arrows travel vertically upward and can burst multiple balloons if they overlap at that x-position
- You need to determine the smallest number of arrows required to burst every balloon
Solution Explanation:
The solution uses a greedy approach with interval scheduling:
- Sort balloons by their end points - This allows us to process balloons in order of where they end
- Track the last arrow position - Initialize with negative infinity
- For each balloon:
- If the balloon's start is greater than the last arrow position, we need a new arrow
- Place the new arrow at the balloon's end position (optimal placement to potentially hit more balloons)
- Increment the arrow count
- Return the total number of arrows used
The key insight is that by sorting by end points and placing arrows at the rightmost position of each balloon group, we maximize the chance of hitting overlapping balloons with a single arrow.
Intuition
Think of this problem as trying to find the minimum number of points that can "cover" all intervals. When multiple balloons overlap at some x-coordinate, a single arrow at that position can burst all of them. So we're looking for the smallest set of x-coordinates where each balloon is hit by at least one arrow.
The key realization is that this is an interval scheduling problem. We want to group overlapping intervals together and shoot one arrow per group.
Why sort by end points? Consider what happens when we process balloons in order of their ending positions:
- When we encounter a balloon that doesn't overlap with our last arrow position, we must shoot a new arrow
- Where should we place this new arrow? At the balloon's end point
- Why the end point? Because it gives us the best chance to hit other balloons that might start before this end point
For example, if we have balloons [1,6]
and [2,8]
:
- If we place an arrow at position
6
(end of first balloon), it hits both - If we place it at position
1
(start of first balloon), it only hits the first one
The greedy choice is always to place the arrow as far right as possible (at the end point) while still hitting the current balloon. This maximizes our coverage for balloons that haven't been processed yet.
By sorting by end points and greedily placing arrows, we ensure that:
- Each arrow is placed optimally to potentially hit future balloons
- We only shoot a new arrow when absolutely necessary (when a balloon starts after our last arrow)
- The total number of arrows is minimized
This approach works because once we've decided to shoot an arrow at a certain position, we never need to reconsider that decision - the greedy choice is always optimal.
Solution Approach
Let's walk through the implementation step by step:
1. Sort the balloons by their end points:
sorted(points, key=lambda x: x[1])
This sorts the points
array based on each balloon's ending position (x[1]
). This ordering is crucial for the greedy algorithm to work correctly.
2. Initialize tracking variables:
ans, last = 0, -inf
ans
: Counts the number of arrows neededlast
: Tracks the position of the last arrow shot (initialized to negative infinity to ensure the first balloon always requires an arrow)
3. Process each balloon in sorted order:
for a, b in sorted(points, key=lambda x: x[1]):
For each balloon, a
represents the start position and b
represents the end position.
4. Check if a new arrow is needed:
if a > last: ans += 1 last = b
- If the current balloon's start position
a
is greater than the last arrow positionlast
, this balloon isn't hit by any previous arrow - We need a new arrow: increment
ans
- Place the new arrow at the current balloon's end position
b
(updatelast = b
)
Why this works:
- When
a > last
, there's no overlap between the current balloon and the position of the last arrow - By placing the arrow at position
b
(the balloon's end), we ensure:- The current balloon is burst
- Any future balloon that overlaps with position
b
can also be burst by this arrow - We maximize the coverage to the right
Time Complexity: O(n log n)
due to sorting, where n is the number of balloons
Space Complexity: O(1)
if we don't count the sorting space, as we only use a constant amount of extra variables
The algorithm processes each balloon exactly once after sorting, making it efficient and optimal for this problem.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with balloons: [[10,16], [2,8], [1,6], [7,12]]
Step 1: Sort by end points After sorting by end points (second element of each interval):
[1,6]
(ends at 6)[2,8]
(ends at 8)[7,12]
(ends at 12)[10,16]
(ends at 16)
Step 2: Process each balloon
Initialize: ans = 0
, last = -โ
Process balloon [1,6]:
- Start position
1 > -โ
? Yes - Need a new arrow!
- Shoot arrow at position
6
(the balloon's end) - Update:
ans = 1
,last = 6
Process balloon [2,8]:
- Start position
2 > 6
? No (2 โค 6) - This balloon is already hit by the arrow at position 6!
- No new arrow needed
- Keep:
ans = 1
,last = 6
Process balloon [7,12]:
- Start position
7 > 6
? Yes - Need a new arrow!
- Shoot arrow at position
12
(the balloon's end) - Update:
ans = 2
,last = 12
Process balloon [10,16]:
- Start position
10 > 12
? No (10 โค 12) - This balloon is already hit by the arrow at position 12!
- No new arrow needed
- Keep:
ans = 2
,last = 12
Result: We need 2 arrows minimum
- Arrow 1 at position 6: bursts balloons
[1,6]
and[2,8]
- Arrow 2 at position 12: bursts balloons
[7,12]
and[10,16]
The key insight: By sorting by end points and placing arrows at the rightmost position of each non-overlapping group, we maximize the number of balloons each arrow can burst.
Solution Implementation
1class Solution:
2 def findMinArrowShots(self, points: List[List[int]]) -> int:
3 # Initialize arrow count and last arrow position
4 arrow_count = 0
5 last_arrow_position = float('-inf')
6
7 # Sort balloons by their end position (right boundary)
8 # This greedy approach ensures we can burst maximum balloons with each arrow
9 sorted_points = sorted(points, key=lambda x: x[1])
10
11 # Iterate through each balloon interval
12 for start, end in sorted_points:
13 # If current balloon's start is beyond the last arrow position,
14 # we need a new arrow
15 if start > last_arrow_position:
16 arrow_count += 1
17 # Place the arrow at the current balloon's end position
18 # to potentially burst more upcoming balloons
19 last_arrow_position = end
20
21 return arrow_count
22
1class Solution {
2 public int findMinArrowShots(int[][] points) {
3 // Sort balloons by their end position (right boundary)
4 // Using comparingInt to avoid integer overflow that would occur with direct subtraction
5 Arrays.sort(points, Comparator.comparingInt(point -> point[1]));
6
7 // Initialize the count of arrows needed
8 int arrowCount = 0;
9
10 // Track the position of the last arrow shot
11 // Initialize to a very small value to ensure first balloon always needs an arrow
12 long lastArrowPosition = -(1L << 60);
13
14 // Iterate through each balloon
15 for (int[] balloon : points) {
16 int startPosition = balloon[0];
17 int endPosition = balloon[1];
18
19 // If current balloon's start is beyond the last arrow's position,
20 // we need a new arrow
21 if (startPosition > lastArrowPosition) {
22 arrowCount++;
23 // Shoot the arrow at the current balloon's end position
24 // This maximizes the chance of hitting subsequent balloons
25 lastArrowPosition = endPosition;
26 }
27 }
28
29 return arrowCount;
30 }
31}
32
1class Solution {
2public:
3 int findMinArrowShots(vector<vector<int>>& points) {
4 // Sort balloons by their end position (right boundary)
5 // This greedy approach ensures we can burst maximum balloons with each arrow
6 sort(points.begin(), points.end(), [](vector<int>& a, vector<int>& b) {
7 return a[1] < b[1];
8 });
9
10 // Initialize arrow count
11 int arrowCount = 0;
12
13 // Track the position of the last arrow shot
14 // Initialize to a very small value to ensure first balloon requires an arrow
15 long long lastArrowPosition = -(1LL << 60);
16
17 // Iterate through each balloon
18 for (auto& balloon : points) {
19 int balloonStart = balloon[0];
20 int balloonEnd = balloon[1];
21
22 // If current balloon's start is beyond the last arrow's position,
23 // we need a new arrow
24 if (balloonStart > lastArrowPosition) {
25 arrowCount++;
26 // Shoot the arrow at the end of current balloon
27 // This maximizes the chance of hitting future balloons
28 lastArrowPosition = balloonEnd;
29 }
30 }
31
32 return arrowCount;
33 }
34};
35
1/**
2 * Finds the minimum number of arrows needed to burst all balloons
3 * @param points - Array of balloon intervals where points[i] = [start, end]
4 * @returns Minimum number of arrows needed
5 */
6function findMinArrowShots(points: number[][]): number {
7 // Sort balloons by their end position in ascending order
8 points.sort((a, b) => a[1] - b[1]);
9
10 // Initialize arrow count
11 let arrowCount: number = 0;
12
13 // Track the position of the last arrow shot
14 let lastArrowPosition: number = -Infinity;
15
16 // Iterate through each balloon interval
17 for (const [startPosition, endPosition] of points) {
18 // If current balloon starts after the last arrow position,
19 // we need a new arrow
20 if (lastArrowPosition < startPosition) {
21 arrowCount++;
22 // Shoot the arrow at the end position of current balloon
23 // to potentially burst more overlapping balloons
24 lastArrowPosition = endPosition;
25 }
26 }
27
28 return arrowCount;
29}
30
Time and Space Complexity
Time Complexity: O(n log n)
where n
is the number of balloons (length of points array).
- The sorting operation
sorted(points, key=lambda x: x[1])
dominates the time complexity, requiringO(n log n)
time to sort all intervals by their end points. - The subsequent for loop iterates through the sorted list once, performing constant-time operations (comparison and assignment) for each element, which takes
O(n)
time. - Therefore, the overall time complexity is
O(n log n) + O(n) = O(n log n)
.
Space Complexity: O(n)
or O(1)
depending on the sorting algorithm implementation.
- The
sorted()
function in Python creates a new sorted list, which requiresO(n)
additional space to store the sorted intervals. - If we consider the space used by the sorting algorithm itself, Python's Timsort typically uses
O(n)
space in the worst case. - The variables
ans
andlast
use constant spaceO(1)
. - Therefore, the overall space complexity is
O(n)
.
Note: If the code were modified to use points.sort(key=lambda x: x[1])
instead of sorted()
, which sorts in-place, the space complexity could be reduced to O(1)
auxiliary space (excluding the space needed for the sorting algorithm's internal operations).
Common Pitfalls
1. Sorting by Start Position Instead of End Position
A common mistake is sorting balloons by their start position rather than their end position. This breaks the greedy algorithm's correctness.
Incorrect approach:
sorted_points = sorted(points, key=lambda x: x[0]) # Wrong!
Why it fails:
Consider balloons: [[1,6], [2,8], [7,12], [10,16]]
- Sorting by start:
[[1,6], [2,8], [7,12], [10,16]]
- After processing [1,6], arrow at position 6
- [2,8] overlaps, no new arrow needed
- [7,12] doesn't overlap with position 6, new arrow at 12
- [10,16] overlaps with 12, no new arrow needed
- Result: 2 arrows
But the optimal solution needs only 2 arrows at positions 6 and 16, which the end-sort approach correctly finds.
Solution: Always sort by end position x[1]
to ensure the greedy choice remains optimal.
2. Integer Overflow with Initial Arrow Position
Using a small initial value like 0 or -1 for last_arrow_position
can cause incorrect results when balloon coordinates include negative numbers.
Incorrect initialization:
last_arrow_position = 0 # Wrong if balloons can have negative coordinates!
Example failure:
For balloons [[-10, -5], [-3, 2]]
, initializing to 0 would incorrectly assume the first balloon is already burst.
Solution: Use float('-inf')
to guarantee the first balloon always requires a new arrow:
last_arrow_position = float('-inf')
3. Incorrect Overlap Check
Using >=
instead of >
when checking if a new arrow is needed leads to unnecessary arrows.
Incorrect check:
if start >= last_arrow_position: # Wrong! This shoots unnecessary arrows arrow_count += 1
Why it's wrong:
If a balloon starts exactly at the last arrow position, that arrow already bursts it. Using >=
would shoot an extra unnecessary arrow.
Solution: Use strict inequality:
if start > last_arrow_position: arrow_count += 1
4. Not Handling Edge Cases
Forgetting to handle special cases like empty input or single balloon.
Missing edge case handling:
def findMinArrowShots(self, points):
# Directly sorting without checking
sorted_points = sorted(points, key=lambda x: x[1])
# ... rest of code
Solution: Add validation (though most LeetCode problems guarantee non-empty input):
if not points: return 0
5. Placing Arrow at Wrong Position
Placing the arrow at the balloon's start position instead of end position reduces the chance of hitting future overlapping balloons.
Suboptimal placement:
if start > last_arrow_position: arrow_count += 1 last_arrow_position = start # Wrong! Less optimal
Why it matters: Placing the arrow at the end position maximizes the rightward coverage, increasing the probability of bursting future balloons that might overlap.
Solution: Always update to the end position:
last_arrow_position = end
What's the relationship between a tree and a graph?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!