S034 · Greedy & Backtracking — When to Use Each
Two decision-making patterns that live either side of DP. Greedy: bet on the best local move and never look back — fast, occasionally wrong. Backtracking: explore every option and undo dead ends — slow, always right. Learn to prove greedy is safe, prune backtracking aggressively, and pick the right tool in 30 seconds.
🎯 Given a fresh problem, decide in 30 seconds whether to try greedy, DP, or backtracking — and prove your choice.
Why this session exists
Greedy, DP, and backtracking are the three strategies for making a sequence of decisions. Rookies pick by vibe and often reach for backtracking when a one-line greedy would do, or write greedy when the problem needs DP. The difference between a senior and a mid-level engineer on any decision problem is a 30-second triage: "is the greedy choice provably safe? no → is the state space small enough to explore with pruning? yes → backtrack." This session gives you that triage plus the two templates you'll reuse for years.
- State the two conditions (greedy-choice property + optimal substructure) that make greedy safe.
- Write the backtracking template — choose, recurse, unchoose — from memory.
- Apply the greedy vs DP vs backtracking decision table to a new problem in under a minute.
- Recognise catastrophic backtracking regex patterns and explain why RE2 exists.
- Solve activity selection, N-queens, all-subsets, and permutations without reference.
Prerequisites
- S027 — Recursion — backtracking is recursion + undo.
- S033 — Dynamic Programming — the middle strategy; you should already know when overlapping subproblems appear.
(a) Intuition · 5 min
Greedy: you owe ₹87 in Indian coins. You grab the largest coin ≤ 87 (₹50), then the largest ≤ 37 (₹20), then ₹10, ₹5, ₹1, ₹1. Six coins, done, never reconsidered. This is greedy — locally best at every step, no lookback.
Backtracking: Sudoku. You try 3 in a cell; it forces another cell into a contradiction; you rewind, try 4; that works two cells later; that also fails; rewind further. You explore, you undo, you try again.
Greedy is a gamble: fast, but only correct if the "locally best move is always globally optimal" property holds. For Indian coins it does. For a bizarre denomination set `{1, 3, 4}` targeting 6, greedy picks 4+1+1 (three coins) while the true optimum is 3+3 (two coins). That's a greedy failure — and the reason DP exists.
Backtracking is exhaustive search with rewind. It's brute force with a "this partial solution is doomed" pruner. Always correct, worst-case exponential.
- Greedy — commit to the locally-best choice, never revisit. O(n log n) typical. Correct only when the greedy-choice property holds; else silently wrong.
- DP — enumerate all decompositions, cache each subproblem's answer. Polynomial. Correct when optimal substructure + overlapping subproblems.
- Backtracking — explore every branch, undo on dead-end, prune aggressively. Worst-case exponential but always finds a solution if one exists. Correct always.
- 1956Kruskal's MSTThe first named greedy algorithm to be proven optimal (via the cut property / exchange argument).
- 1959Dijkstra's shortest pathGreedy on a priority queue. Still runs inside every routing engine.
- 1952Huffman codingGreedy prefix code by repeatedly merging the two lowest-frequency symbols. Optimal — and inside every zip/gzip/JPEG.
- 1971Cook · SAT is NP-completeFormalises the class of problems that (probably) require exponential search — the natural home of backtracking.
- 2010Google · RE2 engineShip a non-backtracking regex engine. Trades features (backreferences) for guaranteed linear time. Cloudflare, YouTube, and grep-alike tools follow.
(b) Visual walkthrough · 15 min
Greedy — activity selection worked example
Meetings (start, end): (1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16).
Greedy rule: sort by end time, pick each meeting whose start ≥ previous end.
Why sort by end time? Exchange argument: given any optimal schedule, replace its first meeting with the earliest-ending compatible one — the number of meetings does not decrease. Repeat for every position. So the greedy schedule matches an optimum in size.
Backtracking — the universal template
Three sacred lines, in order: choose → recurse → unchoose. Miss the unchoose and you leak state into sibling branches; miss the prune and you burn hours on impossible partial solutions.
N-Queens (n=4) — pruning in action
Solution: queens at (0,1), (1,3), (2,0), (3,2). Without the safe() check every branch would recurse to depth n; with it, ~90% of branches die at depth 2–3.
The decision table — how to pick
Locally-best composes to globally-best
- One obvious sort key (end time, cost, weight)
- You can sketch an exchange-argument proof
- ‘Interval scheduling’, ‘MST’, ‘Huffman’ family
- Real-time system needs O(n log n) or better
- Approximation is acceptable if proof fails
All choices matter, subproblems overlap
- Coin change with weird denominations
- Sequence alignment / LCS / edit distance
- Knapsack / partition / subset-sum
- Small state space, many decision points
- ‘Best over all splits’ patterns
‘Find all’ or ‘does any exist’
- N-queens, Sudoku, cryptarithm puzzles
- All permutations / subsets / combinations
- Constraint satisfaction with heavy pruning
- State too big for DP table but decisions prune
- SAT / CSP / theorem proving
Classic winners in each camp
Where each strategy has produced the canonical algorithm
"Greedy is the quick heuristic you use when you don't have time to do it properly. It gives you a good-enough answer."
Greedy is either provably optimal or arbitrarily bad — there is no "good enough" in between unless you have separately proved an approximation ratio. When a greedy is correct, it is correct because the problem has a specific structural property (an exchange argument or matroid structure), and it beats DP outright. When that property is absent, greedy can be worse than optimal by an unbounded factor and gives you no signal that it failed.
Because the greedy answer is usually close on the small hand-made examples you test with, and closeness on examples feels like evidence. It is not. Coin change with 1/5/10/25 is greedy-optimal, so everyone learns it as "greedy works for coins" — then a denomination set like 1/3/4 breaks it, and the failure is silent. You get a valid-looking answer that is simply not the best one.
Same algorithm, one denomination set apart, and no error is raised:
def greedy(coins, target):
coins = sorted(coins, reverse=True)
n = 0
for c in coins:
n += target // c
target %= c
return n if target == 0 else None
def exact(coins, target): # DP - always optimal
INF = float('inf')
dp = [0] + [INF] * target
for t in range(1, target + 1):
for c in coins:
if c <= t: dp[t] = min(dp[t], dp[t-c] + 1)
return dp[target]
print(greedy([1,5,10,25], 30), exact([1,5,10,25], 30)) # 2 2 - agree
print(greedy([1,3,4], 6), exact([1,3,4], 6)) # 3 2 - greedy takes 4+1+1Why does "always pick the meeting that ends earliest" provably maximise the number of non-overlapping meetings? It is not obvious that a local rule can be globally optimal.
- 1Let G be the greedy solution and O any optimal solution, both sorted by finish time. Suppose they agree on the first k choices and differ at choice k+1.forced by · this is the standard exchange setup — compare against an arbitrary optimum and show you never lose by being greedy
- 2Greedy's (k+1)-th pick finishes no later than O's, since greedy picks the earliest-finishing compatible meeting and O's pick was also compatible.forced by · greedy searches the same feasible set and minimises finish time over it
- 3Swap O's (k+1)-th choice for greedy's. The result is still feasible: every later meeting in O started after O's pick ended, and greedy's pick ends no later.forced by · finishing earlier can only widen the remaining window, never narrow it — feasibility is monotone in remaining free time
- 4The swapped solution has the same number of meetings, so it is still optimal, and now agrees with greedy on k+1 choices.forced by · we exchanged one meeting for exactly one meeting
- 5Repeat the exchange; after finitely many steps O has been transformed into G with no loss of size.forced by · each exchange strictly increases the agreement prefix and never decreases the objective
Therefore greedy is optimal — not by luck, but because "finish earliest" is the choice that leaves the largest feasible remainder, and the objective is monotone in that remainder.
And note what this predicts: the proof depends entirely on "earlier finish leaves a superset of options". Change the objective to maximise total duration booked and that property dies instantly — a long meeting can be worth more than two short ones — so greedy fails and you need DP. The exchange argument tells you not just that greedy works but exactly which variant will break it.
Greedy walks through one-way doors. Each choice is final, made on local information, and never revisited. It is fast precisely because it never looks back — and it is correct only if you can prove that no door ever leads somewhere worse than the one you skipped.
Backtracking is an explorer trailing a rope. It commits, explores, and when it hits a dead end it winds the rope back to the last junction and undoes the choice. It is complete — it will find the answer if one exists — but it pays exponentially for that guarantee unless it can prune whole branches without exploring them.
- Greedy needs a proof, not an intuition. If you cannot state the exchange argument, assume it is wrong.
- Backtracking = choose · explore · un-choose. The undo step is where the bugs live; mutate-and-restore is faster than copying but only if the restore is exact.
- Pruning is what makes backtracking usable: kill a branch the instant it cannot beat the best answer so far, or violates a constraint. Without pruning it is just brute force with extra structure.
- The escalation ladder is greedy → DP → backtracking with pruning → approximation. Move up only when the rung below provably fails.
Fire this model the moment you see: interval or meeting scheduling · Huffman coding · minimum spanning tree · task assignment to workers · N-queens / sudoku / constraint satisfaction · "generate all valid combinations" · a query optimiser choosing a join order · a resource allocator deciding what to run next.
You must schedule a large set of jobs onto limited compute. Greedy heuristic, exact search, or a solver?
Online systems use greedy, and that is a correct engineering decision rather than a compromise: a scheduler that takes 30 seconds to produce a perfect plan for a state that changed 29 seconds ago has produced nothing. The optimum of a stale world is worthless.
The pattern worth stealing is to run both on different clocks — greedy in the hot path, and an exact or solver-based run offline against recorded workloads to measure how much the greedy is actually leaving on the table. That number tells you whether the heuristic deserves investment, and without it you are guessing.
(c) Hands-on · 25 min
Save as gb_lab.py, run with python3 gb_lab.py.
"""gb_lab.py — greedy and backtracking classics side by side."""
from __future__ import annotations
from typing import List, Tuple
# ---------- GREEDY: activity selection ----------
def activity_selection(meetings: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
"""Return the maximum set of non-overlapping meetings (list of (start, end))."""
meetings = sorted(meetings, key=lambda m: m[1]) # sort by end time
chosen: List[Tuple[int, int]] = []
last_end = float("-inf")
for s, e in meetings:
if s >= last_end:
chosen.append((s, e))
last_end = e
return chosen
# ---------- GREEDY: coin change — works ONLY for canonical currencies ----------
def coin_change_greedy(coins: List[int], amount: int) -> List[int] | None:
"""Greedy: take biggest coin ≤ remaining. Returns None if it fails."""
coins = sorted(coins, reverse=True)
used: List[int] = []
for c in coins:
while amount >= c:
amount -= c
used.append(c)
return used if amount == 0 else None
# ---------- GREEDY: Huffman-ish — merge two lowest freq (illustrative) ----------
def huffman_cost(freqs: List[int]) -> int:
"""Total merge cost (LeetCode 1046 style). Real Huffman builds the tree; this is the cost."""
import heapq
heap = list(freqs); heapq.heapify(heap)
total = 0
while len(heap) > 1:
a = heapq.heappop(heap)
b = heapq.heappop(heap)
total += a + b
heapq.heappush(heap, a + b)
return total
# ---------- BACKTRACKING: N-Queens ----------
def n_queens(n: int) -> List[List[int]]:
"""Return all N-queens solutions as lists where placed[r] = column of row r."""
solutions: List[List[int]] = []
placed: List[int] = []
def safe(r: int, c: int) -> bool:
for pr, pc in enumerate(placed):
if pc == c or abs(pc - c) == abs(pr - r):
return False
return True
def solve(r: int) -> None:
if r == n:
solutions.append(placed.copy()) # complete → record
return
for c in range(n):
if safe(r, c): # prune
placed.append(c) # choose
solve(r + 1) # recurse
placed.pop() # unchoose
solve(0)
return solutions
# ---------- BACKTRACKING: all subsets (power set) ----------
def all_subsets(nums: List[int]) -> List[List[int]]:
out: List[List[int]] = []
cur: List[int] = []
def solve(i: int) -> None:
if i == len(nums):
out.append(cur.copy())
return
solve(i + 1) # branch: skip nums[i]
cur.append(nums[i]) # choose
solve(i + 1) # branch: include
cur.pop() # unchoose
solve(0)
return out
# ---------- BACKTRACKING: all permutations ----------
def all_permutations(nums: List[int]) -> List[List[int]]:
out: List[List[int]] = []
used = [False] * len(nums)
cur: List[int] = []
def solve() -> None:
if len(cur) == len(nums):
out.append(cur.copy())
return
for i, x in enumerate(nums):
if used[i]:
continue
used[i] = True; cur.append(x)
solve()
used[i] = False; cur.pop()
solve()
return out
# ---------- Demo ----------
if __name__ == "__main__":
meetings = [(1,4),(3,5),(0,6),(5,7),(3,9),(5,9),
(6,10),(8,11),(8,12),(2,14),(12,16)]
print("Activities:", activity_selection(meetings)) # 4 meetings
print("Coins for 87 (Indian):", coin_change_greedy([1,2,5,10,20,50,100,500], 87))
print("Coins for 6 (weird [1,3,4]) — greedy:", coin_change_greedy([1,3,4], 6))
# ↑ Greedy picks 4+1+1 (3 coins); optimum is 3+3 (2 coins). GREEDY FAILS.
print("Huffman cost [4,3,2,6]:", huffman_cost([4, 3, 2, 6]))
print("4-queens solutions:", n_queens(4)) # 2
print("8-queens count:", len(n_queens(8))) # 92
print("subsets of [1,2,3]:", all_subsets([1, 2, 3])) # 8
print("permutations of [1,2,3]:", all_permutations([1, 2, 3])) # 6Anatomy of the script
Add this to the end of gb_lab.py:
import time
def n_queens_unpruned(n: int) -> int:
count = 0
placed: list[int] = []
def safe(r: int, c: int) -> bool:
for pr, pc in enumerate(placed):
if pc == c or abs(pc - c) == abs(pr - r):
return False
return True
def solve(r: int) -> None:
nonlocal count
if r == n:
if all(safe(i, placed[i]) for i in range(n)): # check only at the leaf
count += 1
return
for c in range(n):
placed.append(c) # NO PRUNING — always recurse
solve(r + 1)
placed.pop()
solve(0)
return count
for n in [6, 8]:
t0 = time.time(); k1 = len(n_queens(n))
t1 = time.time(); k2 = n_queens_unpruned(n)
t2 = time.time()
print(f"n={n}: pruned {k1} in {t1-t0:.3f}s · unpruned {k2} in {t2-t1:.3f}s")At n=8 the pruned version finishes in milliseconds; the unpruned version explores 8⁸ = 16.7 million branches. Same answers, wildly different wall clock. That gap is why the if safe(...) check exists.
(d) Production reality · 15 min
A new WAF rule containing the regex (?:(?:\"|'|\]|\}|\\|\d|(?:nan|infinity|true|false|null|undefined|symbol|math)|\`|\-|\+)+[)]*;?((?:\s|-|~|!|{}|\|\||\+)*.*(?:.*=.*))) shipped worldwide. Turns out one input pattern caused the PCRE backtracking engine to explore ~2ⁿ states.
CPU pinned to 100% across every edge server for 27 minutes. Cloudflare's own dashboards went down along with the customers.
Immediate: killswitch the WAF globally. Long term: migrate to RE2, Google's non-backtracking regex engine, which guarantees O(n × m) time. Add a per-regex time budget in staging so pathological patterns die before they ship.
Naive greedy — assign each new request to the geographically nearest driver — leaves clusters of drivers stranded near "hot spots" while faraway requests wait.
Batch-window greedy with a "future value" score: every 3 seconds, run the Hungarian algorithm (an O(n³) optimal assignment) over the current batch, weighting each driver-request pair by expected wait time and the driver's forecasted next request. It's greedy over 3-second windows and DP-adjacent inside the window.
Finding memory-safety bugs by naive fuzzing misses complex conditions that require specific 8-byte values to hit a vulnerable branch. Random input can't produce them in useful time.
Symbolic execution + a SMT solver (Z3) — the solver internally uses backtracking with clause learning (the modern DPLL algorithm) to construct inputs that satisfy path constraints. Backtracking is the reason bugs like Heartbleed-alikes get found in hours instead of years.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
- When would you reach for greedy over DP? (name the property)
- What is the three-line backtracking template? (in order)
- Why is
RE2a thing? (one sentence, connect to catastrophic backtracking)
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.