1. Introduction & Motivation
This lesson is a collection, not a single derivation. It covers four classic interview problems that all live in the "greedy over an array" family, but each one earns its greedy correctness in a genuinely different way. There is no single template that unifies them — that is exactly the point. A candidate who has only memorized "sort, then scan" for interval problems will be stuck the moment the array is circular (Gas Station), the constraint is bidirectional (Candy), the objects are ranges instead of points (Interval Stabbing), or the goal is to close a group as early as possible (Partition Labels).
What ties the four together is method, not formula: each problem has a small piece of local information (a running tank level, a neighbor comparison, an arrow position, a last-seen index) that is sufficient to make an irrevocable decision at each step without ever needing to reconsider it. Learning to find that piece of information — and to prove it is sufficient — is the actual transferable skill.
2. Prerequisites
This lesson assumes you can already read and modify a single-pass array scan, and that you are comfortable with the general shape of a greedy correctness argument (exchange argument, staying-ahead argument, or a direct contradiction proof). Two closely related greedy-on-arrays problems are not re-derived here because they are already covered in full elsewhere in this curriculum:
- Jump Game I & II — reachability and minimum jumps via a running "farthest reachable index," fully proved and implemented in DSA Part 16: Greedy Proofs & Scheduling § Jump Game.
- Merge Intervals (and the closely related Non-overlapping Intervals) — sort-by-start-time interval merging, fully proved and implemented in DSA Part 16: Greedy Proofs & Scheduling § Interval Scheduling.
If either of those feels unfamiliar, read that lesson's sections first — this one builds on the same "sort, then make one linear pass" instinct without repeating the setup.
3. A Concrete Example
Hand-tracing Gas Station
There are 5 gas stations arranged in a circle. gas[i] is the fuel available at station i; cost[i] is the fuel needed to drive from station i to station i+1. Starting with an empty tank at some station, can a car complete the full circuit? gas = [1, 2, 3, 4, 5], cost = [3, 4, 5, 1, 2].
Try starting at index 0 and track the running tank level (tank += gas[i] - cost[i]) at each station:
| Station i | gas[i] | cost[i] | gas[i] − cost[i] | tank after station i | Outcome |
|---|---|---|---|---|---|
| 0 | 1 | 3 | −2 | −2 | Tank negative — station 0 cannot be the start |
| 1 | 2 | 4 | −2 | −2 | Tank negative — station 1 cannot be the start |
| 2 | 3 | 5 | −2 | −2 | Tank negative — station 2 cannot be the start |
| 3 | 4 | 1 | +3 | 3 | Tank stays non-negative |
| 4 | 5 | 2 | +3 | 6 | Tank stays non-negative — circuit complete |
Every time the tank goes negative, the candidate start resets to the very next station and the tank resets to 0. Station 3 is the last candidate standing, and it never goes negative again through the rest of the array. That candidate, index 3, is the answer: start there and the tank never runs dry.
Section 4 explains exactly why "reset to the next index the instant the tank goes negative" is safe, rather than merely a convenient heuristic.
4. Gas Station (Circular Tour Feasibility)
LeetCode 134. Given gas[] and cost[] for n stations arranged in a circle, return the starting station index from which a car with an initially empty tank can complete the entire circuit exactly once, or −1 if no such station exists. It is guaranteed that if a solution exists, it is unique.
Plain-English steps:
- If total gas is less than total cost, no starting station works — return −1 immediately.
- Otherwise, a valid start exists. Initialize
candidate = 0andtank = 0. - Scan
ifrom 0 ton−1. Addgas[i] − cost[i]totank. - If
tankdrops below 0, the run starting atcandidatehas failed ati. Setcandidate = i + 1and resettank = 0. - After the scan,
candidateis the answer.
Why skipping past the failure point is valid
The key fact to prove is: if the tank goes negative while attempting a start at index $s$, no index in $[s, j]$ can be a valid start, where $j$ is the index where the tank first went negative. This licenses jumping the candidate all the way to $j+1$ instead of retrying every index between $s$ and $j$ individually — turning an $O(n^2)$ brute force into a single $O(n)$ pass.
Define the prefix sum $P(s, i) = \sum_{k=s}^{i} \big(gas[k] - cost[k]\big)$, the tank level after starting at $s$ and driving through station $i$. Suppose starting at $s$ fails first at $j$, meaning $P(s, i) \geq 0$ for every $i$ with $s \leq i \lt j$, but $P(s, j) \lt 0$. Now consider any candidate start $s'$ with $s \lt s' \leq j$. Because the run from $s$ to $s'-1$ never went negative, $P(s, s'-1) \geq 0$. Splitting the sum,
$$P(s, j) = P(s, s'-1) + P(s', j)$$
so $P(s', j) = P(s, j) - P(s, s'-1) \leq P(s, j) \lt 0$. Starting at $s'$ also runs the tank negative by the time it reaches $j$ — it fails too, and possibly earlier. So none of $s+1, s+2, \ldots, j$ can be the answer, which is exactly why the algorithm is allowed to advance the candidate directly to $j+1$ and reset the tank, without re-examining the skipped indices.
The remaining question is why some start must succeed at all when $\sum gas \geq \sum cost$. Suppose the single forward scan from index 0 (with resets) ends with final candidate $c$. By construction, every index from 0 up to $c-1$ was ruled out by the argument above, each ruling-out consuming exactly one contiguous failed run. Because $\sum_{i=0}^{n-1}(gas[i]-cost[i]) = \sum gas - \sum cost \geq 0$, the total of all these consumed negative segments plus the final segment starting at $c$ must sum to something non-negative overall; since every prior segment was strictly negative (that is why it failed), the final segment from $c$ to the end of the array — wrapping around to close the circuit — cannot itself be negative at any prefix, or the same argument would have advanced the candidate again. This is the standard "total surplus forces a working start to exist" argument, and it is why the algorithm needs only one linear pass with the total-sum precheck, never a second pass to confirm the candidate.
flowchart TD
A["Precheck: total gas < total cost?"] -->|Yes| Z["Return -1: no start is possible"]
A -->|No| B["candidate = 0, tank = 0, i = 0"]
B --> C["tank += gas[i] - cost[i]"]
C --> D{"tank < 0 ?"}
D -->|Yes| E["No start in [candidate, i] can work either"]
E --> F["candidate = i + 1, tank = 0"]
F --> G["i = i + 1"]
D -->|No| G
G --> H{"i < n ?"}
H -->|Yes| C
H -->|No| I["Return candidate as the answer"]
The diagram shows the same rule traced in Section 3: the moment the tank dips below zero, every station from the current candidate through the failing index is eliminated in one step, and the search resumes immediately after the failure.
5. Candy / Distribute Candies
LeetCode 135. There are $n$ children standing in a line, each with a rating. Every child must receive at least 1 candy, and any child with a higher rating than an adjacent neighbor must receive strictly more candies than that neighbor. Find the minimum total number of candies needed.
Plain-English steps:
- Give every child 1 candy to start:
candies[i] = 1for all $i$. - Left-to-right pass: for each $i$ from 1 to $n-1$, if
ratings[i] > ratings[i-1], setcandies[i] = candies[i-1] + 1. After this pass, every child's count already respects its left neighbor. - Right-to-left pass: for each $i$ from $n-2$ down to 0, if
ratings[i] > ratings[i+1], setcandies[i] = max(candies[i], candies[i+1] + 1). Themaxis essential: it must not discard a larger value already earned from the left-to-right pass. - Sum
candies[]for the answer.
Why does the second pass never break the first pass's guarantee? The right-to-left pass only ever increases candies[i] (via max), and it only touches candies[i] when ratings[i] > ratings[i+1] — a fact about the right neighbor that the left-to-right pass never examined. Raising candies[i] can only help it stay strictly greater than candies[i-1] when required (never hurt, since the left-to-right relationship was `>` and increasing candies[i] preserves any existing `>`), so both directional constraints hold simultaneously at the end.
| i | rating | after left-to-right pass | after right-to-left pass |
|---|---|---|---|
| 0 | 1 | 1 | 1 |
| 1 | 2 | 2 (2>1, so 1+1) | 2 |
| 2 | 3 | 3 (3>2, so 2+1) | 3 |
| 3 | 2 | 1 (2 is not > 3) | 2 (2>1 to the right, so max(1, 1+1)) |
| 4 | 1 | 1 (1 is not > 2) | 1 (no right neighbor) |
For ratings = [1, 2, 3, 2, 1], the left-to-right pass alone gives [1, 2, 3, 1, 1] — correct on the left side, but child 3 (rating 2) has only 1 candy despite outranking child 4 (rating 1, also 1 candy), a tie that violates the rule. The right-to-left pass fixes exactly that, raising child 3 to 2 candies. Final distribution: [1, 2, 3, 2, 1], total 9 candies.
6. Interval Stabbing (Minimum Arrows to Burst Balloons)
Each balloon is a horizontal interval [start, end] (its diameter along the x-axis). An arrow shot straight up at some x-coordinate bursts every balloon whose interval contains that coordinate. Given all the balloon intervals, find the minimum number of arrows needed so every balloon is pierced by at least one arrow.
This is the same greedy pattern family as interval covering / non-overlapping intervals: sort by one coordinate, then make a single greedy pass. Concretely: sort the balloons by their end coordinate. Shoot the first arrow at the end of the first (unpierced) balloon. That single arrow pierces every other balloon whose start is at or before that arrow position — skip all of them. The first balloon whose start is strictly past the current arrow needs a new arrow, shot at its end. Repeat. The shared exchange-argument reasoning for why sorting by the coordinate you extend greedily is safe is developed in full for the interval-covering variant in Interval Scheduling: Minimum Meeting Rooms & Point Covering § Interval Covering; this section focuses on applying that pattern to the balloon-arrow framing rather than re-deriving the proof.
Trace on balloons [[10,16],[2,8],[1,6],[7,12]], sorted by end: [1,6], [2,8], [7,12], [10,16].
- Arrow at 6 (end of
[1,6]): pierces[1,6]and also[2,8], since2 ≤ 6 ≤ 8. Both skipped. Arrows so far: 1. - Next unpierced balloon is
[7,12], whose start (7) is past the current arrow (6): fire a new arrow at 12. It also pierces[10,16], since10 ≤ 12 ≤ 16. Arrows so far: 2. - All balloons pierced. Answer: 2.
7. Partition Labels
LeetCode 763. Given a string, split it into as many partitions as possible such that each letter appears in at most one partition. Return the length of each partition, in order.
max(current end, last occurrence of the current character). When the scan index reaches the current partition's end, that partition is complete and cannot be split further — close it and start a new one.
Plain-English steps:
- Build a map from each character to the index of its last occurrence in the string.
- Initialize
start = 0andend = 0for the current partition. - Scan index
ifrom 0 to the end of the string. Setend = max(end, last[s[i]])— the current partition must stretch at least that far, because the current character cannot appear again in a later partition. - If
i == end, every character seen so far in this partition has had its last occurrence accounted for, so no future character can force this partition to grow further. Record the partition lengthend - start + 1, then setstart = i + 1to begin a new partition.
Trace on s = "aabbbacccd". Last occurrences: a→5, b→4, c→8, d→9.
| i | char | last[char] | end = max(end, last[char]) | i == end? | Action |
|---|---|---|---|---|---|
| 0 | a | 5 | 5 | no | continue |
| 1 | a | 5 | 5 | no | continue |
| 2 | b | 4 | 5 | no | continue |
| 3 | b | 4 | 5 | no | continue |
| 4 | b | 4 | 5 | no | continue |
| 5 | a | 5 | 5 | yes | close partition [0,5], length 6; start = 6 |
| 6 | c | 8 | 8 | no | continue |
| 7 | c | 8 | 8 | no | continue |
| 8 | c | 8 | 8 | yes | close partition [6,8], length 3; start = 9 |
| 9 | d | 9 | 9 | yes | close partition [9,9], length 1 |
Result: partition lengths [6, 3, 1] — "aabbba", "ccc", "d". The 'a' at index 5 is what forces the first partition to swallow the 'b's in between: without the second 'a', the partitions would have split earlier.
8. Complexity Comparison Table
| Problem | Core Idea | Time | Space |
|---|---|---|---|
| Gas Station | Running tank level; reset candidate start whenever tank goes negative | $O(n)$ | $O(1)$ |
| Candy | Two passes: left-to-right enforces left-neighbor rule, right-to-left enforces right-neighbor rule via max | $O(n)$ | $O(n)$ |
| Interval Stabbing (Arrows) | Sort by interval end; greedily place an arrow at the end of the first unpierced interval | $O(n \log n)$ | $O(1)$ extra (excluding sort) |
| Partition Labels | Track each character's last occurrence; extend and close partitions in one pass | $O(n)$ | $O(1)$ extra (fixed alphabet) |
9. Applications & Alternatives
Where these patterns show up in practice
- Fuel and route planning: the Gas Station feasibility check generalizes to any circular resource-replenishment route — delivery loops, EV charging-stop planning, and fuel-depot placement along a fixed circuit.
- Fair resource distribution: the Candy two-pass pattern (satisfy a local ordering constraint from both directions) appears in seat/queue-priority assignment and in any bidirectional monotonicity constraint over a sequence.
- Calendar and reservation systems: Interval Stabbing is the same primitive behind "minimum number of checkpoints/inspectors to cover every scheduled time window" and shares its sort-by-end scan with meeting-room and resource-booking systems.
- String and data chunking for parallel processing: Partition Labels' last-occurrence scan is the same idea used to split a log file or byte stream into independent, non-overlapping chunks that can be processed by separate workers without any chunk needing to reference another.
10. Exercises
Easy
- Run Partition Labels by hand on
s = "ecbacbced"and list the resulting partition lengths.
Medium
- Given
gas = [4, 3, 1, 2, 7, 4]andcost = [1, 2, 7, 3, 2, 5], find the starting station using the Gas Station algorithm, and verify by hand-tracing the tank level that it never goes negative from that start.
Hard
- Suppose Candy adds a rule that no child may receive more than
maxCandiescandies (a fixed cap). Explain, without writing code, why the simple two-pass greedy can no longer guarantee a valid assignment always exists, and describe at a design level what would need to change (e.g., what makes a rating sequence infeasible under the cap, and whether the answer is still uniquely determined by local neighbor comparisons alone).
[start, end] during which a single driver must be dispatched to cover that request, and a single driver dispatch can cover every request whose window contains the dispatch time. Determine the minimum number of drivers needed to guarantee every request is covered, without being told which of today's four techniques applies.Review rhythm: tomorrow, reconstruct all four core insights from memory without looking at the code. Next week, re-implement Gas Station and Partition Labels from scratch and re-derive the Candy two-pass argument out loud. Several weeks later, when you meet a new problem, ask whether it is really the Jump Game / Merge Intervals pattern from Part 16, or one of these four — the fastest way to fail an interview on this family is misidentifying which of the five is in front of you.
11. Summary Cheat Sheet
- Gas Station: total gas ≥ total cost guarantees a start exists; track a running tank, reset the candidate to the next index whenever the tank goes negative.
- Candy: two passes — left-to-right for the left-neighbor rule, right-to-left with
maxfor the right-neighbor rule — never undoing each other. - Interval Stabbing (Minimum Arrows, LC 452): sort by interval end, greedily place an arrow at the first unpierced balloon's end, skip everything it also pierces. Not the same problem as DP Burst Balloons (LC 312).
- Partition Labels: track each character's last occurrence; a partition closes exactly when the scan index catches up to the running max of last-occurrence indices.
- All four run in $O(n)$ or $O(n \log n)$ time with little to no extra space beyond the output — the hard part is finding the right local invariant, not the code.