LeetCode #452 Medium

Minimum Number of Arrows to Burst Balloons

Balloons are given as horizontal intervals. An arrow shot upward at x bursts every balloon whose interval contains x. Return the minimum number of arrows needed.

Constraints
  • 1 <= points.length <= 10⁵
  • points[i].length == 2
  • -2³¹ <= xstart < xend <= 2³¹ - 1
intervalsgreedysorting
Open on LeetCode ↗
02

Intuition

Minimum number of arrows to burst balloons finds the fewest vertical arrows needed to pop every balloon, where each balloon is a horizontal interval and an arrow bursts all balloons it passes through. This is an interval-covering problem, and the sorting key decides whether the greedy works: - Sort by end coordinate, then shoot at the end of the first balloon — that arrow bursts every balloon it can, and nothing is lost by placing it there. The argument is that the balloon ending earliest must be burst by some arrow, and placing that arrow at its right edge is optimal: any arrow further left bursts a subset of what this one does. So the greedy choice never sacrifices a later opportunity. After placing an arrow, skip every balloon whose start is at or before that arrow's position — those are already burst. The next balloon starting after it needs a new arrow. Sorting by start instead is the classic error. It leads to shooting at the wrong position and overcounting on inputs where a wide balloon starts early and ends late. The overlap test must be <=, since a balloon merely touching the arrow's position is still burst — the endpoints are inclusive. Coordinates can reach the limits of 32-bit integers, so computing a midpoint or a difference can overflow in some languages. Comparing directly rather than computing spans avoids the issue. Every balloon is examined once after the sort, so the cost is dominated by sorting at O(n log n), with O(1) extra space.

How to spot this pattern

Interval scheduling again: sort by end coordinate and fire an arrow at the end of the first balloon, which pops every balloon overlapping it. Choosing the earliest possible end maximises what a single arrow covers — the same exchange argument as Non-overlapping Intervals.

03

Approach

Try it first

Before reading on: price up what sorting first costs here, then ask what sorting by one endpoint makes obvious that was hidden before. Aim for O(n log n) time and O(1) space.

1

Sort by end coordinate

Sorting by end is what makes the greedy correct. Sorting by start leads to arrows in the wrong position and overcounts on wide early balloons.

2

Shoot at the first balloon's end

The earliest-ending balloon must be burst, and its right edge is the best position — any arrow further left bursts strictly fewer balloons.

3

Skip everything already burst

Advance past every balloon whose start is at or before the arrow's position. These are covered and need no further arrows.

4

Use inclusive comparison

Test with <= — a balloon merely touching the arrow's position is still burst, since the interval endpoints are inclusive.

5

Place a new arrow on a gap

The first balloon starting after the arrow's position requires a new arrow at its own end. Repeat until every balloon is covered.

6

Watch for coordinate overflow

Coordinates can reach the 32-bit limits, so computing midpoints or spans can overflow. Compare endpoints directly instead.

7

Cost of the approach

Sorting dominates at O(n log n) time, with a single O(n) pass afterwards and O(1) extra space.

04

Solution & live demo

▶1class Solution:
▶2 def findMinArrowShots(self, points):
▶3 if not points:
▶4 return 0
▶5 points.sort(key=lambda x: x[1])
▶6 arrows = 1
▶7 pos = points[0][1]
▶8 for s, e in points[1:]:
▶9 if s > pos:
▶10 arrows += 1
▶11 pos = e
▶12 # else: the current arrow already bursts it
▶13 return arrows
05

Common pitfalls

Sorting by start coordinate

✗ Wrong
points.sort(key=lambda x: x[0])
✓ Right
points.sort(key=lambda x: x[1])

Firing at a balloon's start can miss later balloons that a slightly further shot would catch. Anchoring at the earliest end guarantees the arrow is as far right as it can be while still bursting the current balloon.

Using >= for the new-arrow test

✗ Wrong
if s >= pos:
✓ Right
if s > pos:

A balloon starting exactly where the arrow was fired is still touched by it — the ranges are inclusive. The strict test correctly reuses the arrow; >= fires an unnecessary extra one.

Sorting with subtraction on large coordinates

✗ Wrong
sort(points.begin(), points.end(), [](auto&a, auto&b){ return a[1] - b[1] < 0; });
✓ Right
return a[1] < b[1];

Coordinates reach ±2^31, so a[1] - b[1] overflows a 32-bit int and produces an inconsistent comparator — which can corrupt the sort or crash. Compare directly instead of subtracting.

06

Edge cases

Single balloon

One arrow suffices.

All balloons overlapping a common point

One arrow bursts them all.

Balloons touching at exactly one coordinate

Still burst by a single arrow — the <= comparison is what gets this right.

Completely disjoint balloons

Each needs its own arrow, so the answer equals the count.

07

Complexity

Time
O(n log n)
Space
O(1)
Sorting dominates. Structurally identical to Non-overlapping Intervals.