LC Patterns — Greedy
The Master Insight (read before the patterns)
Greedy is DP with the search deleted: at every step there is one choice that is provably safe — some optimal solution definitely contains it — so you commit and never look back. No memo table, no branching, usually one sort + one pass, O(n log n).
The catch: greedy is the only technique where the algorithm is trivial and the proof is the deliverable. Any plausible-sounding local rule compiles and runs; half of them are wrong. In an interview, "sort by end time and pick" earns nothing by itself — the sentence why that's safe is what's being graded.
The three proof tools — know their names, keep a one-liner for each:
- Exchange argument (the workhorse): take any optimal solution that disagrees with greedy at the first point; swap in greedy's choice; show the result is no worse. Repeat → greedy is optimal.
- Staying ahead: after every step k, greedy's partial state is ≥ any other strategy's state (e.g. "the earliest finishing time so far is minimal") — induct to the end.
- Invariant / structural: define a quantity greedy maximizes at all times ("furthest reachable index") and show the answer is a function of it.
Greedy vs DP litmus test (say it when choosing): "Can I rank candidate choices at this step without knowing how the future plays out? If sorting makes the best choice locally obvious and an exchange argument holds — greedy. If choices interact with the future — DP." When unsure in an interview: state the greedy rule, actively search for a counterexample for 30 seconds out loud, and fall back to DP if one appears. That visible verification loop is itself a strong signal.
§0Mechanics & Universal Pitfalls
Greedy has no data-structure boilerplate — the "template" is a discipline:
1. SORT by the key that makes the greedy choice safe ← 80% of the thinking
2. One pass, maintaining a small invariant (1–3 variables)
3. Per element: commit / skip / extend, per the rule
4. PROOF: one spoken sentence — exchange, staying-ahead, or invariantUniversal pitfalls
- Wrong sort key. Sorting by start when the proof needs end (interval selection), by absolute value when it needs the difference (two-city). The exchange argument dictates the key — derive it, don't guess. If you can't finish the sentence "sorting by X is safe because swapping any two adjacent items ordered otherwise can't help", the key is wrong.
- No counterexample search. The classic trap problems (e.g. coin change with arbitrary denominations — LC 322) look greedy and aren't. Spend 30 seconds trying to break your own rule before coding; interviewers plant these.
- Ties. Equal ends, equal starts, equal counts — decide tie order consciously; several problems hide their hardest cases in ties.
- Claiming greedy without the proof. Even when correct, an unjustified greedy reads as a lucky guess. One sentence fixes it.
- Greedy on the wrong quantity. "Maximize items" ≠ "maximize value" — interval count problems sort by end; weighted versions are DP (LC 1235). Check which one you were asked.
- Off-by-one in invariant updates — updating
furthestbefore vs after the boundary check (jump problems), popping vs peeking order (stack greedy). Trace a 4-element example by hand.
1Interval Selection (sort by earliest end) ★
What it is: From n intervals, select the maximum number of mutually non-overlapping ones (equivalently: remove the minimum to make the rest disjoint, or pierce all with the fewest arrows). Sort by end; take every interval that starts after the last taken end.
Intuition: the interval that ends earliest leaves the most room for everything after it — choosing it can never hurt. This is the exchange argument to know cold: if an optimal solution's first interval isn't the earliest-ending one, swap the earliest-ending one in; it ends no later, so everything downstream still fits. "Min removals" is just n − "max selected"; "min arrows" is the same scan counting groups that share a point.
Approach: sort by end; last_end = -inf; count = 0; for each [s, e]: if s >= last_end (or >, boundary semantics per problem): take it, count += 1, last_end = e. O(n log n), O(1) extra.
Challenges & pitfalls
sorting by start instead (the natural-but-wrong instinct — a long early-starting interval blocks everything); touching endpoints — does [1,2],[2,3] overlap? (435: no; 452: balloons at 2 share an arrow — yes); reciting the exchange argument fluently; recognizing that the weighted variant is NOT this pattern (job scheduling with profits → DP + binary search, LC 1235).
[[10,16], [2,8], [1,6], [7,12]]; a vertical arrow at x pops every balloon covering x.- Sort by end:
[1,6], [2,8], [7,12], [10,16]. - Fire an arrow at the first balloon's end,
x = 6: it also covers[2,8](since 2 ≤ 6 ≤ 8) → both popped. Next uncovered balloon is[7,12]; fire atx = 12: it also covers[10,16]→ both popped. - 2 arrows. ✓ Firing at the earliest end greedily pops as many overlapping balloons as possible before committing the next arrow — the exchange argument guarantees it's optimal.
2Interval Merge / Insert (sort by start)
What it is: Combine overlapping intervals into disjoint blocks (merge), or splice one new interval into an already-sorted disjoint list (insert). Sort by start; sweep, extending the current block while intervals touch it, emitting it when a gap appears.
Intuition: once sorted by start, an interval either overlaps the block under construction (s <= cur_end → absorb: cur_end = max(cur_end, e)) or starts a new block. The max matters: a later interval can be nested inside the block and must not shrink it. Insert (57) is merge specialized: emit all intervals ending before the new one, absorb all that overlap it, emit the rest — three phases, no sort needed.
Approach: sort by start; iterate keeping cur = [s0, e0]; overlap → extend, gap → append cur, restart. Remove-covered (1288): sort by start asc, end desc, count intervals whose end exceeds the running max end.
Challenges & pitfalls
forgetting max() on extension (the nested-interval bug — the #1 error here); emitting the final block after the loop; touching vs overlapping semantics ([1,2],[2,3] merge in 56); in 57, the three-phase boundaries; in 1288, justifying the descending end tie-break out loud.
[[1,3], [2,6], [8,10], [15,18]].- Sort by start (already sorted). Keep a current block, extend while intervals overlap it:
[1,3]starts the block.[2,6]:2 ≤ 3→ overlaps → extend end tomax(3, 6) = 6→ block[1,6].[8,10]:8 > 6→ gap → emit[1,6], start block[8,10].[15,18]:15 > 10→ gap → emit[8,10], start[15,18]. Emit it at the end.- Merged
[[1,6], [8,10], [15,18]]. ✓ Themaxon extension is what stops a nested interval (say[2,4]) from wrongly shrinking the block.
3Coverage & Jumps (furthest-reach invariant)
What it is: You advance along a line — array indices, a garden, a timeline — and each position/item extends how far you can reach. Questions: can you reach the end (feasibility), or with how few jumps/segments (optimization). One pass maintaining furthest reachable.
Intuition: you never need to decide which jump you took — only the invariant "everything up to furthest is reachable." Feasibility (55): scan i; if i > furthest you're stranded; else furthest = max(furthest, i + nums[i]). Minimum jumps (45): implicit BFS on a line — the current jump covers a window; when the scan crosses the window's end, one more jump is spent. Coverage problems (taps 1326, video stitching 1024) are the same after converting each item to an interval [i - r, i + r]: greedily pick, among intervals starting within reach, the one extending furthest. Proof: greedy maximizes furthest after every step, and any strategy's reach is ≤ greedy's (staying ahead).
Approach: feasibility: one variable. Min segments: jumps, cur_end, furthest = 0, 0, 0; for i in 0..n-2: furthest = max(furthest, i + nums[i]); if i == cur_end: jumps += 1; cur_end = furthest. Coverage-by-intervals: precompute the furthest end reachable from each start, then run the same window walk.
Challenges & pitfalls
iterating to n−1 in 45 (the last index must not spend an extra jump — loop to n−2); the stranded check before the update in 55; in coverage problems, detecting "gap ⇒ impossible"; resisting the O(n²) DP; explaining the BFS-levels view of 45.
[0, 5], tap ranges [3, 4, 1, 1, 0, 0] (tap at position i waters [i − r, i + r]).- Convert taps to intervals: the tap at position 1 has range 4 → covers
[1−4, 1+4] = [-3, 5], which already spans the whole garden[0, 5]. - So 1 tap suffices. ✓ The greedy "reach as far right as possible from within the currently-covered prefix" collapses to one interval here; in general you'd repeatedly jump to the furthest-reaching tap that starts within your current coverage.
nums = [2, 3, 1, 1, 4]. Track furthest: index 0 → 2, index 1 → max(2, 4) = 4 ≥ last index → reachable → true. ✓ You never track which jumps, only how far the reachable prefix extends.4Sorted Two-Set Matching (greedy pairing with two pointers)
What it is: Two collections must be paired off — cookies to children, people into boats, your cards against theirs — optimizing matches. Sort both; walk with two pointers, always resolving the most constrained element first.
Intuition: after sorting, the extremes carry the decision. Boats (881): the heaviest person is the constraint — pair them with the lightest; if even that fails, the heaviest sails alone (no better partner will ever exist — that's the whole proof). Cookies (455): the smallest cookie that satisfies the smallest child — wasting a big cookie on an easy child can only hurt. Advantage shuffle (870): beat each of their cards with your cheapest winning card; if your best can't beat their best, sacrifice your worst against it.
Approach: sort both sides; two pointers moving inward (881: lo/hi on one array) or forward in lockstep (455). Per step: try the greedy match; on success advance both, on failure advance the constrained side only (or burn the sacrifice card). O(n log n) sort dominates.
Challenges & pitfalls
which side is "most constrained" — always argue from the element with the fewest options; advancing the wrong pointer on failure; 870 needs indices preserved through sorting; the sacrifice logic (fight vs concede) benefits from stating both branches before coding.
people = [3, 2, 2, 1], each boat holds at most 2 people with total weight ≤ limit = 3. Fewest boats?- Sort:
[1, 2, 2, 3]. Two pointers,lo(lightest),hi(heaviest): lo=1, hi=3:1 + 3 = 4 > 3→ the heaviest (3) can't share with anyone → sails alone. Boat 1. Movehiin.lo=1, hi=2:1 + 2 = 3 ≤ 3→ pair them. Boat 2. Move both.lo=2, hi=2(same index): one person left → sails alone. Boat 3.- 3 boats. ✓ Pairing the heaviest with the lightest (and sending them alone if even that fails) is provably optimal — no lighter person would help the heaviest more.
5Exchange-Argument Custom Sort (the comparator IS the algorithm)
What it is: Items must be ordered or assigned to maximize a global objective — concatenate numbers into the largest string, split people between two cities cheapest, order flowers by growing time. The entire solution is: find the pairwise comparison that says "a before b is better", sort by it, sweep.
Intuition: if a single swap of adjacent items never helps, the arrangement is optimal (any permutation is reachable by adjacent swaps). So define the comparator directly from the objective: largest number (179): a before b iff string a+b > b+a. Two cities (1029): sort by costA - costB (how much you regret sending them to A), send the first half to A — the difference, not the absolute cost, is the decision variable. Flowers (2136): sort growing time descending so long growers start earliest.
Approach: write the objective for two adjacent items in both orders, derive the inequality, hand it to the sort. Then one linear pass computes the answer. Cousin worth knowing: 406 (Queue Reconstruction) — sort by height desc, k asc, then insert each person at index k.
Challenges & pitfalls
deriving the comparator instead of guessing ("sort desc" fails 179 on [3, 30] — the canonical counterexample, know it); proving the comparator is transitive/consistent when challenged; in 1029 articulating why the delta is what you sort; testing the comparator on a 2-element example by hand before running.
nums = [3, 30, 34, 5, 9]; concatenate into the largest possible number.- Sort with the comparator "
abeforebiffa+b(as strings) >b+a". This decides3vs30by comparing"330"vs"303"→"330"bigger →3before30. Likewise9beats everything ("9…"), then5, then34,3,30. - Sorted order →
"9","5","34","3","30"→ concatenated "9534330". ✓ Plain descending sort would put30before3and produce"9534303", which is smaller — that[3, 30]case is exactly why you derive the comparator instead of guessing.
6Running Accumulator with Reset (prefix feasibility)
What it is: One pass, one accumulator (tank, profit, running sum), and a rule for when the accumulated state is worthless — reset and restart from here. The insight is always the same: a prefix that failed can be discarded wholesale.
Intuition: gas station (134): if you start at A and die at C, no station between A and C can succeed either — they'd enter C with less gas than you had. So restart from C+1; and total gas ≥ total cost ⇒ some start works. Stock I (121): the running minimum is the only past state that matters — best profit ending today = price - min_so_far. Stock II (122): with unlimited trades, every up-tick is independently collectible — answer = Σ max(0, p[i] − p[i−1]).
Approach: one loop, 2–3 variables. 134: tank, start, total; on tank < 0: start = i + 1, tank = 0; return start if total >= 0 else −1. 121: track min_price, best. 122: sum positive deltas. The entire interview is the justification.
Challenges & pitfalls
these look trivial after you know them — the risk is presenting the code without the proof and getting drilled; for 134, the two lemmas (restart-past-failure + total-feasibility) must both be stated; for 122, skepticism that ignoring trade boundaries is legal; resisting over-engineering (if you're writing more than 10 lines, you've missed the pattern).
gas = [1,2,3,4,5], cost = [3,4,5,1,2]; find a start index to complete the loop, or −1.- Total gas 15 = total cost 15, so a solution exists (first lemma). Sweep tracking
tank: - Starting the running tank at index 0:
1−3 = −2 < 0→ any start in[0,0]fails → restart at index 1, reset tank. 2−4 = −2 < 0→ restart at 2.3−5 = −2 < 0→ restart at 3.- From index 3:
4−1 = 3, then+5−2 = 6→ survives to the end. - Start index 3. ✓ The key lemma: once the tank goes negative at some point, no earlier start in that failed stretch could have done better, so you restart past it — one pass, not O(n²).
7Lexicographic Greedy (monotonic stack surgery)
What it is: Build the lexicographically smallest/largest result under a budget — remove k digits, keep one of each letter, choose a subsequence of length k. A stack holds the answer-so-far; each new element pops everyone it beats while the budget (removals left, duplicates available) allows.
Intuition: lexicographic order is decided at the first position of difference — so earlier positions dominate everything after. A bigger digit sitting before a smaller one should be evicted (if you may still remove) because improving an earlier position beats any later consequence. The stack stays monotonic; the budget is the only thing gating the pops. All three listed problems are one skeleton with a different pop-guard.
Approach: iterate; while stack and stack[-1] is wrong-order vs x and ⟨budget allows⟩: pop; push x (subject to its own guard — 316 skips letters already in the stack). Post-process: 402 trims leftover k from the tail and strips leading zeros; 1673 truncates to k. The transferable skill is writing the pop-guard: 402 → k > 0; 316 → last_occurrence[stack[-1]] > i; 1673 → len(stack) - 1 + (n - i) >= k.
Challenges & pitfalls
deriving the remaining-budget guard for 1673; 316's dual condition (pop only if the letter recurs later; never push a letter already present); leading zeros and the empty-string edge in 402; explaining why earlier positions dominate ("first point of difference decides lexicographic order").
num = "1432219", k = 3; remove 3 digits for the smallest result.- Keep a stack; when a new digit is smaller than the top and we still have removals, pop (a bigger digit in an earlier position is bad):
1→[1].4→[1,4].3 < 4, pop 4 (k=2) →[1,3].2 < 3, pop 3 (k=1) →[1,2].2→[1,2,2].1 < 2, pop 2 (k=0) →[1,2,1].9→[1,2,1,9].- Budget spent, take the stack → "1219". ✓ Each pop improved an earlier, more significant position — which dominates everything after it in lexicographic order.
8Greedy Segmentation (cut when the current piece is self-contained)
What it is: Split a sequence into the fewest/most pieces subject to a per-piece constraint — every letter confined to one piece, no repeated character in a piece. Sweep once, tracking what the current piece still owes; cut the moment the debt hits zero (or must-cut when the constraint would break).
Intuition: two dual forms. Max pieces (763): each letter's presence obligates the piece to extend to that letter's last occurrence; maintain piece_end = max(last[c] for c seen); when i == piece_end the piece is closed and cutting immediately is safe (nothing inside is needed later, and cutting as early as possible only creates more pieces). Min pieces / forced cut (2405): extend until adding s[i] would violate (duplicate char) → start a new piece there.
Approach: 763: precompute last[c] in one pass; second pass with start, piece_end; on i == piece_end emit i - start + 1. 2405: a seen-set, reset on violation, count resets + 1. Both O(n), O(alphabet).
Challenges & pitfalls
believing the earliest-cut is safe in 763 (rehearse: "any valid partition has a cut at or after mine"); the piece-end update must consider every character seen, not just the current one; in min-cut forms, off-by-one on where the new piece starts; distinguishing this from DP partitioning (132 needs DP).
s = "ababcbacadefegdehijhklij"; cut into the most pieces so each letter appears in only one piece.- First record each letter's last index (e.g.
alast at 8,cat 7,bat 5, …). - Sweep, extending the piece to the furthest last-index of any letter seen so far:
- From index 0, letters
a, b, cpullpiece_endout to 8 (a's last). Ati == 8the piece closes → length 9 ("ababcbaca"). - Next piece from 9: letters
d, e, f, g→piece_end15 → length 7. Last piece → length 8. - Answer
[9, 7, 8]. ✓ Cutting the instantireaches the runningpiece_endis safe and maximizes the number of pieces.
9Count-Based Feasibility (greedy consumption from a multiset)
What it is: Decide whether a multiset of numbers can be partitioned into groups of a required shape — k consecutive cards, pairs (x, 2x), consecutive runs of length ≥ 3. Greedily consume from a canonical end (smallest first) via a hash counter: the smallest remaining element has the fewest options, so its group is forced.
Intuition: the smallest unused value can only ever be the head of its group (nothing smaller exists to precede it) — so its group is fully determined: take x, x+1, …, x+k−1 (846) or match x with 2x (954). If the forced group can't be formed, no arrangement exists (nothing else can absorb that head). 659 refines this with lookahead: prefer extending an existing run over starting a new one.
Approach: Counter + iterate values in sorted order; for each value with count > 0, consume its forced group, decrementing counters; fail fast on any missing member. 659's two-counter version: need[x] and avail[x]. O(n log n) for the sort.
Challenges & pitfalls
954's negatives (−4 pairs with −8, not −2 — sort by abs); decrementing counts for the whole group atomically vs re-reading stale counts; 659's greedy priority (extend > start); saying the forcing argument crisply: "the minimum element has no candidates below it, so its role is forced — greedy just executes forced moves."
hand = [1,2,3,6,2,3,4,7,8], groupSize = 3; can it be split into consecutive triples?- Counts:
{1:1, 2:2, 3:2, 4:1, 6:1, 7:1, 8:1}. The smallest value present must be the head of a group: - smallest is
1→ forced group[1, 2, 3]; decrement those counts. - smallest now
2→ group[2, 3, 4]; decrement. - smallest now
6→ group[6, 7, 8]; decrement. - All counts hit zero → true. ✓ Because the minimum value can't sit anywhere but at the front of its run, each group is forced; if any required member were missing, we'd immediately return false.
10Regret Greedy with a Heap (commit now, undo the worst later)
What it is: Greedy where commitments are revocable: take the cheap default every time, record each decision in a heap, and when a resource runs dry (fuel, time budget, bricks), undo the single most profitable past decision and continue. Full treatment in heaps Pattern 9 — this entry places it on the greedy map.
Intuition: pure greedy fails when you can't rank present options without seeing the future; regret greedy sidesteps the lookahead by making the cheapest reversible choice and deferring the real decision to the moment of failure — when you finally have the information, one heap pop applies the best correction. The proof is still an exchange argument, applied at the failure point.
Approach: scan in the naturally-sorted order (position for 871, deadline for 630); per item take the default and push the alternative's value; on a constraint violation, pop the extreme (max-heap to undo the costliest, min-heap to downgrade the cheapest commitment) until feasible again.
Challenges & pitfalls
recognizing the tell — "greedy feels right but a counterexample nags, and there's a depletable resource"; the polarity of the heap per problem; 630 requires sorting by deadline first — regret greedy almost always sits on top of a Pattern-1/5-style sort.
bricks and ladders; a ladder covers any climb, a brick covers one unit of height.- Greedy default: use a ladder on every climb, remembering each ladder-climb's height in a min-heap of size = ladders. When a new climb arrives and all ladders are "in use," the smallest recorded ladder-climb should really have been bricks (ladders are too precious to spend on small climbs) → pop it and pay for it with bricks.
- When bricks would go negative, you can't advance → that building is the answer. ✓ You never had to decide up front which climbs deserve ladders — you retroactively downgraded the cheapest ladder-climb to bricks only when forced. (See the heaps file for the full trace.)
11Local Constraint Sweeps (one- and two-pass neighbor rules)
What it is: Each position has a constraint involving only its immediate neighbors — more candies than a lower-rated neighbor, flowers not adjacent, correct change from what you've collected. Solve with one linear sweep (constraint looks one way) or two opposing sweeps combined with max (constraint looks both ways).
Intuition: Candy (135): "more than the left neighbor if higher-rated" and "more than the right neighbor if higher-rated" are independent one-directional constraints — satisfy each with its own directional pass, then max(left[i], right[i]) satisfies both minimally (the max of two minimums is minimal for the conjunction). Flowers (605): a slot is plantable iff both neighbors are empty right now — planting greedily left-to-right never hurts. Lemonade (860): always break a $20 with $10+$5 rather than three $5s, because $5 bills are strictly more useful (a dominance argument).
Approach: identify whether the constraint is one-directional (single pass with counters) or bidirectional (two passes + combine). 135: two O(n) passes + max. 605: single pass, mutate-or-count with sentinel empties at both ends. 860: track two counters (fives, tens), branch by bill.
Challenges & pitfalls
trying to solve 135 in one pass (possible but fiddly — the two-pass version is the interview answer); 605 boundary cells (sentinels beat special-casing); in 860, articulating the dominance reason for preferring $10+$5; recognizing the signature — constraints of radius 1 decompose into directional sweeps.
[1, 0, 2]; each child gets ≥ 1 candy, and any child gets more than a lower-rated neighbor. Minimize the total.- Left-to-right pass (satisfy the left-neighbor rule): start everyone at 1 →
[1, 1, ?]; index 2 (rating 2) > index 1 (rating 0) →left[2] = left[1] + 1 = 2→[1, 1, 2]. - Right-to-left pass (right-neighbor rule): index 0 (rating 1) > index 1 (rating 0) →
right[0] = right[1] + 1 = 2→[2, 1, 1]. - Combine with max per child:
max(1,2), max(1,1), max(2,1) = [2, 1, 2]→ total 5. ✓ Each directional pass is the provable minimum for its own constraint; the max of the two is the minimum satisfying both.
▤Summary Table — Recognition Cheat Sheet
| # | Pattern | Trigger phrase in problem | Sort key / invariant | Proof tool |
|---|---|---|---|---|
| 1 | Interval selection | "max non-overlapping", "min removals/arrows" | sort by end | exchange (earliest end is safe) |
| 2 | Interval merge/insert | "merge", "insert interval", "covered" | sort by start (+ end desc for covered) | sweep invariant |
| 3 | Coverage & jumps | "can you reach", "min jumps/taps" | furthest reachable | staying ahead / BFS levels |
| 4 | Two-set matching | pair people/items across two arrays | sort both, two pointers | most-constrained-first exchange |
| 5 | Custom sort | arrange/assign to optimize a global objective | comparator from pairwise objective | adjacent-swap argument |
| 6 | Accumulator + reset | circular tour, single-pass profit | running sum; reset on failure | failed prefix is discardable |
| 7 | Lexicographic stack | "lexicographically smallest", "remove k" | monotonic stack + budget guard | first-difference dominance |
| 8 | Segmentation | "partition so each piece ⟨self-contained⟩" | running obligation (piece_end) | earliest cut is safe |
| 9 | Count feasibility | "can you split into groups of ⟨shape⟩" | Counter, consume smallest first | forced-move argument |
| 10 | Regret greedy | greedy + depletable resource | heap of revocable choices | exchange at failure point |
| 11 | Local sweeps | neighbor-radius-1 constraints | directional pass(es), combine with max | per-direction minimality |