Skip to content
DSA Patterns

Learn

Greedy

Greedy explained: the exchange argument that proves a local choice is safe, when greedy beats DP, and the classic problems — Gas Station, Jump Game, Partition Labels, Candy.

3 min readUpdated 2 Sept 2026

#Greedy#Sorting#Proof#Optimization

Greedy is the pattern with the smallest code and the highest risk. Take the locally best option at each step and never revisit it — which is either optimal or badly wrong, with nothing in between. The interview skill is not writing greedy code; it is justifying that greedy applies.

When greedy works

Two conditions must hold:

  1. Greedy choice property — a locally optimal choice is part of some globally optimal

solution.

  1. Optimal substructure — after making that choice, the rest is the same problem on a

smaller input.

The standard proof is the exchange argument: take any optimal solution that differs from the greedy one, swap in the greedy choice, and show the result is no worse. You do not need a formal write-up in an interview — but you do need the sentence. "Sorting by end time is safe because finishing earliest leaves the most room for everything after it" is what turns a guess into an answer.

When you cannot make that argument, the choice interacts with future ones and it is DP. Coin Change is the standard counterexample: greedily taking the largest coin fails on coins [1, 3, 4] for amount 6 — greedy gives 4 + 1 + 1, optimal is 3 + 3.

Sort, then sweep

Most greedy problems begin with a sort that makes the safe choice obvious.

def find_content_children(g, s):        # Assign Cookies
    g.sort()                            # child greed
    s.sort()                            # cookie sizes
    child = cookie = 0
    while child < len(g) and cookie < len(s):
        if s[cookie] >= g[child]:
            child += 1                  # satisfied: move to the next child
        cookie += 1
    return child

Giving the smallest sufficient cookie to the least greedy child is safe by exchange: any optimal assignment can be rewritten to make that pairing without satisfying fewer children.

The interval problems — Minimum Number of Arrows, Non-overlapping Intervals — are the same shape sorted by end time.

Track a running best

The other family needs no sort, just one scan with a well-chosen invariant.

def can_jump(nums):                     # Jump Game
    reach = 0
    for i, jump in enumerate(nums):
        if i > reach:
            return False                # stranded
        reach = max(reach, i + jump)
    return True
def can_complete_circuit(gas, cost):    # Gas Station
    if sum(gas) < sum(cost):
        return -1                       # no solution exists at all
    start = tank = 0
    for i in range(len(gas)):
        tank += gas[i] - cost[i]
        if tank < 0:                    # cannot reach i + 1 from start
            start = i + 1               # so no station in start..i can work either
            tank = 0
    return start

Gas Station's insight is worth stating explicitly: if you run dry between start and i, then no station in that range can be a valid start either — each one begins with less fuel than you had. That collapses an O(n²) search to O(n).

Partition Labels

Greedy with a precomputed lookahead. Each letter must appear in exactly one part, so the current part cannot end before the last occurrence of every letter it contains.

def partition_labels(s):
    last = {ch: i for i, ch in enumerate(s)}
    out, start, end = [], 0, 0
    for i, ch in enumerate(s):
        end = max(end, last[ch])
        if i == end:                    # nothing inside reaches further
            out.append(i - start + 1)
            start = i + 1
    return out

Two-pass greedy: Candy

The one that teaches the most. Each child needs more candy than a lower-rated neighbour on

both sides, and a single pass cannot satisfy both directions at once. So do two, and take

the maximum:

def candy(ratings):
    n = len(ratings)
    candies = [1] * n
    for i in range(1, n):               # left to right: fix the left neighbour
        if ratings[i] > ratings[i - 1]:
            candies[i] = candies[i - 1] + 1
    for i in range(n - 2, -1, -1):      # right to left: fix the right one
        if ratings[i] > ratings[i + 1]:
            candies[i] = max(candies[i], candies[i + 1] + 1)
    return sum(candies)

Whenever a constraint pulls in two directions, one pass per direction and a max at the end is the reflex to have.

Complexity

O(n log n) when a sort leads, O(n) when it does not — which is precisely why greedy is worth proving rather than abandoning for a safe O(n²) or a DP table.

Mistakes that cost the round

  • Applying greedy without an argument. If you cannot say why the local choice is safe,

it probably is not.

  • Sorting by the wrong key. End time versus start time changes the answer entirely.
  • Missing the global feasibility check. Gas Station needs sum(gas) >= sum(cost)

separately; the scan finds where to start, not whether a start exists.

  • One pass on a two-sided constraint, as in Candy.
  • Confusing "works on the examples" with "is correct". Greedy failures are usually

invisible on small inputs.

What to drill

  1. Assign Cookies — sort and sweep.
  2. Partition Labels — precomputed lookahead.
  3. Jump Game — a running reach.
  4. Gas Station — the restart argument.
  5. Hand of Straights — greedy with a counter.
  6. Valid Parenthesis String — track a range of possible open counts.
  7. Candy — two passes.

All on the 22 DSA Patterns sheet.

Frequently asked

How do I know if a problem can be solved greedily?

It needs two properties: a locally optimal choice must be part of some globally optimal solution, and the remainder after that choice must be the same problem on a smaller input. The practical test is whether you can give an exchange argument — take any optimal solution, swap in the greedy choice, and show it is no worse. If you cannot, use DP.

What is the difference between greedy and dynamic programming?

Greedy commits to one choice at each step and never reconsiders; DP explores every choice and keeps the best. Greedy is faster — usually O(n log n) against DP's polynomial table — but only correct when the greedy choice property holds. Coin Change with coins [1,3,4] is the standard example where greedy fails and DP does not.

Why does Gas Station work in one pass?

If the tank goes negative somewhere between start and i, then no station in that range can be a valid start either — any later start begins with strictly less accumulated fuel. So the search can jump straight to i + 1 instead of retrying each station, turning O(n²) into O(n). The separate sum(gas) >= sum(cost) check decides whether any answer exists.

Related