Learning/Greedy

Greedy

8 questions. The shortest code in the entire set, and the hardest to justify.

Never claim greedy without an argument. The argument is the deliverable; the code is usually five lines.

The counterexample to keep loaded

Greedy is a claim about the problem, not a technique
Greedy is a claim about the problem, not a technique

Coins {1, 3, 4}, amount 6: greedy takes 4 + 1 + 1 = 3 coins, the optimum is 3 + 3 = 2. The greedy step is locally optimal at every turn and globally wrong. Name this the moment an interviewer asks "can't you just be greedy?" about something that needs DP.

Three kinds of justification

Every question here uses one of these, and knowing which one you're using is most of the interview.

ArgumentShapeQuestions
ExchangeAny optimal solution can be rewritten to use my choice, no worse8 (prefer ( over *)
Reachability / exactnessI'm not choosing — I'm computing the exact frontier2, 3, 8
Forced moveThere is only one legal option, so no optimality proof is needed5
Monotone and cappedAdding more can only help and can never overshoot6
Block eliminationOne failure kills a whole range of candidates1, 4

The strongest position is the forced move (Q5): the smallest remaining card must start its group, so there is nothing to prove beyond legality. The weakest is a bare exchange argument — which is why Q8's is the only one in the set.

The two problems where a plausible greedy is wrong

Jump Game II — "each jump goes as far as it can" fails, because the furthest landing spot may have the worst onward reach.

Two greedy rules on the same array
Two greedy rules on the same array

Measured: wrong on 233 of 4,000 random arrays — about 6%, the frequency that survives casual testing and fails in production.

Merge Triplets — checking coverage before disqualification looks harmless because LeetCode's own negative example returns false either way. On [[2,2,5],[1,1,2]] against target [2,2,2] it flips the answer to true.

Discarding a prefix: the same argument twice

Kadane: extend or restart
Kadane: extend or restart

Kadane (Q1) drops the running sum when it turns negative. The test nums[i] > cur + nums[i] simplifies to cur < 0 — only the sign matters, never the length.

A negative tank disqualifies a whole range of starts
A negative tank disqualifies a whole range of starts

Gas Station (Q4) resets the start when the tank goes negative — and skips every candidate in between, because each was reached with a non-negative prefix and would arrive with even less. Same reasoning, applied to feasibility rather than optimisation, and it's what turns O(n²) into one pass.

Deferring a decision instead of making one

Track a range, not a count
Track a range, not a count

Valid Parenthesis String (Q8) never decides what a * means. The set of reachable open-counts is always a contiguous interval, so [lo, hi] replaces 3^n branches — with two deliberately asymmetric guards: hi < 0 is fatal, lo < 0 is clamped.

Several of these aren't really greedy

Worth being honest about, because the label misleads:

The genuine greedy choices — where a different choice was available and had to be ruled out — are Kadane's discard, Gas Station's reset, and Q8's preference for ( over * in the stack formulation.

Complexity

QuestionTimeSpace
Maximum SubarrayO(n)O(1)
Jump GameO(n)O(1)
Jump Game IIO(n)O(1)
Gas StationO(n)O(1)
Hand of StraightsO(n log n)O(n)
Merge TripletsO(n)O(1)
Partition LabelsO(n)O(1)int[26]
Valid Parenthesis StringO(n)O(1)

Seven of the eight are O(n) / O(1). Hand of Straights needs the sorting bound because adjacency is order information.

The traps

TrapSymptom
Seeding Kadane's best at 0 (Q1)All-negative input returns 0 — wrong on 4,000/4,000
Updating best before cur (Q1)Wrong on 1,365/4,000; [-2,5] returns −2
Extending the frontier before the reachability check (Q2)[1, 0, 5] returns true
Looping to n-1 instead of n-2 (Q3)[2,3,1,1,4] returns 3, not 2
Hopping as far as each jump allows (Q3)Wrong on 233/4,000
Merging total and tank into one variable (Q4)gas=[2,3,4], cost=[3,4,3] returns 2 instead of −1
int have = count.get(c) (Q5)NPE on an absent key
Checking coverage before disqualification (Q6)[[2,2,5],[1,1,2]] / [2,2,2] returns true
Building last[] in a reverse loop (Q7)Silently records first occurrences
Not clamping lo at 0 (Q8)"(*)" returns false
Testing hi == 0 at the end (Q8)Demands every interpretation balance; "(*)" returns false

Verification

Every snippet compiled and cross-checked against an independent implementation — 30,000+ randomized cases:

Separately, every numeric claim in the prose and in all five diagrams is recomputed by a harness — 97 assertions covering worked examples, traces, measured failure rates and diagram cell values. Two of my own claims were wrong and were corrected by it: