Search Tech Journey

Find topics, journeys and posts

6-month learning plan34 / 130
back to blog
pythonbeginner 55m read

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.

🧩DSAM03 · Data Structures & Algorithms· Session 034 of 130 90 min

🎯 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.

You will be able to
  • 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



(a) Intuition · 5 min

Making change vs solving Sudoku
🌍 Real world

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.

💻 Code world

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.

The three strategies for sequences of decisions
  • 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.
  1. 1956
    Kruskal's MST
    The first named greedy algorithm to be proven optimal (via the cut property / exchange argument).
  2. 1959
    Dijkstra's shortest path
    Greedy on a priority queue. Still runs inside every routing engine.
  3. 1952
    Huffman coding
    Greedy prefix code by repeatedly merging the two lowest-frequency symbols. Optimal — and inside every zip/gzip/JPEG.
  4. 1971
    Cook · SAT is NP-complete
    Formalises the class of problems that (probably) require exponential search — the natural home of backtracking.
  5. 2010
    Google · RE2 engine
    Ship 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

Reach for Greedy

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
Reach for DP

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
Reach for Backtracking

‘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 classics
Dijkstra shortest path · Prim / Kruskal MST · Huffman coding · fractional knapsack · Gale-Shapley stable matching · interval scheduling · Boruvka's parallel MST.
greedy
DP classics
Fibonacci · edit distance · LCS · 0/1 knapsack · matrix chain · Bellman–Ford · CYK parsing · Viterbi decoding · Smith–Waterman DNA alignment.
DP
Backtracking classics
N-queens · Sudoku · Hamiltonian path · graph colouring · SAT (DPLL) · regex matching · maze solving · all-permutations / all-subsets / all-combinations.
backtrack

Common misconception
✗ What most people think

"Greedy is the quick heuristic you use when you don't have time to do it properly. It gives you a good-enough answer."

✓ What is actually true

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.

Why the myth is so sticky

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.

Prove it to yourself

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+1
From first principles
Start with the question

Why 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.

  1. 1
    Let 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
  2. 2
    Greedy'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
  3. 3
    Swap 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
  4. 4
    The 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
  5. 5
    Repeat 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

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.

Mental modelOne-way door vs. explorer with a rope

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.
🔔 Fires when you see

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.

The tradeoff

You must schedule a large set of jobs onto limited compute. Greedy heuristic, exact search, or a solver?

Greedy heuristic
+ you gain O(n log n), runs in milliseconds at any scale, trivially explainable to an operator at 3am, and re-runs cheaply when the input changes — which for a live scheduler it does constantly
− you pay no optimality guarantee unless proved, and no signal when it is far off; a pathological input degrades silently rather than failing
pick when decisions must be made continuously and quickly, and a suboptimal-but-instant answer beats an optimal-but-late one — which describes every online scheduler in existence
Backtracking with pruning
+ you gain exact optimum, and strong pruning often makes the practical runtime vastly better than the exponential worst case suggests
− you pay the worst case really is exponential and you cannot predict which input triggers it, so runtime is unbounded; needs a timeout and a fallback plan
pick when the instance is small and fixed, the decision is made offline, and optimality has real money attached — capacity planning, not request routing
Constraint / MILP solver
+ you gain you declare constraints and objective instead of writing search code; decades of solver engineering handles the pruning far better than you will, and you get an optimality gap you can report
− you pay a heavy dependency, a modelling skill your team may not have, opaque failure modes, and runtimes that are hard to bound in a production SLA
pick when constraints are numerous and interacting, the problem changes shape often, and there is a clear objective to optimise — the standard answer for genuine capacity and allocation planning
What a senior engineer actually does

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]))  # 6

Anatomy of the script

`sorted(meetings, key=lambda m: m[1])`
The greedy commitment. Everything else in activity_selection is a one-pass sweep. If you change the key to `m[0]` (start time) or `m[1]-m[0]` (duration), you get suboptimal answers on adversarial inputs.
greedy-key
`coin_change_greedy` failure on `[1,3,4]`
The single most important observation in the file. Greedy is a hypothesis, not a proof. Always check with a counterexample or an exchange argument.
counterexample
`safe()` inside `n_queens`
The pruner. Without it, 8-queens explores 8⁸ = 16.7M branches. With it, ~2k. Pruning is 99% of what makes backtracking practical.
prune
`placed.append(c) / .pop()`
The choose/unchoose pair. Every append must have a matching pop, else state leaks across siblings. If you find this fragile, pass immutable state instead.
undo
`all_subsets` branches without pruning
This one truly enumerates 2ⁿ subsets — no pruning is possible because every subset is a valid answer. Backtracking = exhaustive enumeration when we do want everything.
exhaustive
`all_permutations` uses `used[]` array
The classic ‘which elements have I already placed?’ tracker. In interviews, some prefer swapping in-place; the boolean array is easier to reason about and just as fast.
state
Try itFeel the difference pruning makes

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.

💡 Hint · Time both versions. The pruned one should be roughly 10⁴× faster at n=10.

(d) Production reality · 15 min

War story Cloudflare · global outage· 201927 minutes of global 502s · half the internet
🔥 What broke

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.

🧯 The fix

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.

🎓 Lesson to steal
Backtracking regex is a footgun at internet scale. If the pattern's untrusted or the input's untrusted, use RE2 (or its Rust cousin `regex`). If you must use PCRE, add timeouts and staging fuzz tests.
Post-mortem
War story Uber · dispatch / ride matching· 2020millions of matches per hour · global
🔥 What broke

Naive greedy — assign each new request to the geographically nearest driver — leaves clusters of drivers stranded near "hot spots" while faraway requests wait.

🧯 The fix

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.

🎓 Lesson to steal
Real production is rarely pure greedy or pure DP — it's greedy over a time window, with a smarter scoring function tuned by ML. The pattern (window + optimal solve inside) works for ad auctions, kernel schedulers, and CDN cache admission.
Post-mortem
War story Google · SAT/SMT via Z3 in ClusterFuzz· 2016hundreds of thousands of security bugs found
🔥 What broke

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.

🧯 The fix

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.

🎓 Lesson to steal
Modern SAT/SMT solvers are the state of the art in "smart backtracking" — they learn from every dead end to prune the tree exponentially. Any hard combinatorial problem where you can express constraints in first-order logic is one Z3 call away from an answer.
Post-mortem

Where this shows up in the rest of the plan

Greedy, DP, and backtracking are the three pillars
S037 · SQL Joins
The join reorderer is a DP; the physical operator picker is greedy over cost estimates.
S041 · Indexes
The optimiser greedily picks the ‘cheapest’ index for each predicate.
S066 · Search — Dijkstra & A*
Dijkstra is greedy on a priority queue; A* adds a heuristic score.
S072 · Load balancing
Kubernetes scheduler, HAProxy, envoy — all greedy on per-request scores.
S095 · Reinforcement Learning
ε-greedy exploration is literally the greedy strategy plus a random-move budget.
S110 · Regex engines
Python `re` backtracks; RE2 does not. The tradeoff shows up in every log parser you build.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

  1. When would you reach for greedy over DP? (name the property)
  2. What is the three-line backtracking template? (in order)
  3. Why is RE2 a 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.