Coding Drills ยท 09

Dynamic Programming

Fourteen drills that turn exponential recursion into a table filled once. Every one follows the same recipe โ€” define the state, write the recurrence, pick a fill order, then squeeze the memory โ€” applied to five shapes: linear sequences, knapsacks, grids, two-string tables and interval tables.

14
Drills
1
Easy
13
Medium
0
Hard
01
Chapter One ยท Core Idea

What Dynamic Programming Really Is

f(5) f(4) f(3) f(3) f(2) f(2) f(1) f(2) f(1) gold: f(3) solved twice red: f(2) solved three times

Dynamic programming (DP) is recursion with a memory. A problem qualifies when it has two properties: optimal substructure โ€” the answer for a big input is built from answers for smaller inputs โ€” and overlapping subproblems โ€” the same smaller inputs are asked for again and again. The naive recursion for Fibonacci, Climbing Stairs or Coin Change re-solves identical subproblems an exponential number of times (see the tree: f(3) twice, f(2) three times, and it gets worse with every level). DP solves each distinct subproblem exactly once and stores the result, so the running time becomes (number of states) ร— (work per state).

There are two equivalent ways to write it. Top-down memoisation keeps the recursive function and adds a cache (a HashMap or array) checked at the top of each call. Bottom-up tabulation allocates the table explicitly and fills it in an order where every dependency is already computed. All fourteen drills below use bottom-up โ€” no recursion depth limit, no hashing overhead, and the fill order makes it obvious which rows can be thrown away to save memory.

Same answer, no repeated work โ€” tabulation of fib(0..7)
BOTTOM-UP TABLE โ€” fib(i) = fib(i-1) + fib(i-2), filled left to right 0 i=0 1 i=1 1 i=2 2 i=3 3 i=4 5 i=5 8 i=6 13 i=7 5 + 8 = 13 Every cell is written once and read twice: 8 cells, O(n) time. The recursion tree above needs O(2^n) calls.
Analogy: A tax accountant who files the same family every year keeps last year's worksheet. Instead of recomputing the depreciation schedule from the purchase date, they read last year's closing figure and apply one year of change. The worksheet is the DP table; "one year of change" is the recurrence; never redoing old years is the whole speed-up.
The four questions to answer before writing any DP code:
  • State: what does dp[i] (or dp[i][j]) mean, in one precise sentence? Most wrong DPs have a vague state.
  • Recurrence: how is a state built from strictly smaller states? Enumerate the last decision (last step, last coin, last character).
  • Base cases & order: which states need no recursion, and in what loop order are all dependencies ready?
  • Answer & memory: which cell holds the final answer, and which old rows can be dropped?
02
Chapter Two ยท Mental Model

How to Spot and Shape a DP

Recognising a DP is mostly about the wording of the question and the shape of the input. The chain below is the reasoning that turns "this looks like brute force" into a table.

The question asks for a count ("how many ways"), an optimum ("minimum / maximum") or feasibility ("can we")
โ–ถ
You do not need the actual objects, only a number per state โ€” a strong DP signal (listing ALL objects instead is backtracking, page 10).
Brute force makes a sequence of choices (step 1 or 2, take or skip, which coin, which letter)
โ–ถ
Name the state as "what is left after the choices so far" โ€” a prefix length, a remaining amount, a grid cell.
Different choice sequences reach the same state (1+2 and 2+1 both reach step 3)
โ–ถ
Overlapping subproblems โ€” caching collapses an exponential tree into one cell per state.
The best answer for a state only needs the best answers of its sub-states
โ–ถ
Optimal substructure โ€” the recurrence is a min/max/sum over the last decision.
Each state reads only a few neighbouring states (i-1, i-2, the row above)
โ–ถ
Memory can shrink to rolling variables or one row โ€” O(n) to O(1), O(mn) to O(n).
A single greedy rule provably dominates every other choice (Jump Game reach)
โ–ถ
The table collapses to one running value โ€” DP was the proof, greedy is the implementation.

Once you know it is DP, the second decision is which shape. Walk down the flow and stop at the first "yes" โ€” the drills on this page are grouped exactly this way.

State = one index; f(i) needs only f(i-1), f(i-2)? yes 1D linear โ€” rolling variables drills 81, 82, 84, 87 no f(i) must scan every earlier j (best j before i)? yes "Ending at i" scan โ€” O(n^2) drills 85, 86 no Pick items to reach an exact sum / capacity? yes Knapsack โ€” unbounded or 0/1 drills 83, 92 no Moves only right / down on a grid? yes 2D grid DP โ€” row by row drills 89, 90, 91 no Two strings, or one substring range s[i..j]? yes Two-sequence / interval table drills 93, 94 no Can one greedy rule replace the whole table? yes Greedy collapse โ€” O(1) state drills 88
03
Chapter Three ยท Pattern Toolkit

Four Reusable DP Templates

Four templates cover all fourteen drills. Learn the loop skeleton and the memory trick of each; per problem you then only change the state meaning and the one-line recurrence.

Template A โ€” Linear recurrence
dp[i] depends on a constant number of earlier cells. Used by Climbing Stairs, House Robber, Decode Ways and (with two running values) Maximum Product Subarray.
Pseudocode โ€” Template A (linear, O(1) space)
function linear(n): prev2 = base(0), prev1 = base(1) for i = 2 to n: cur = combine(prev1, prev2, input[i]) // e.g. prev1 + prev2, max(prev1, prev2 + x) prev2 = prev1 prev1 = cur return prev1
Template A โ€” linear recurrence with rolling variables
ROLLING WINDOW โ€” only two earlier answers are ever read dp[0] dp[1] dp[2] dp[3] dp[4] dp[5] dp[6] dp[7] dp[8] prev2 prev1 cur already forgotten not yet computed After computing cur: prev2 = prev1, prev1 = cur. The array collapses to two variables: O(n) time, O(1) space.
Template B โ€” Knapsack over sums
dp[s] answers "what about exact total s?". Unbounded (items reusable) for Coin Change; 0/1 (each item once) for Partition Equal Subset Sum. Word Break is the unbounded form with words instead of coins.
Pseudocode โ€” Template B (knapsack over sums)
function knapsack(items, target): dp = array[target + 1], dp[0] = identity // 0 coins / true / 1 way // UNBOUNDED (Coin Change): items may repeat -> ascending sums for s = 1 to target: for each item c with c <= s: dp[s] = better(dp[s], dp[s - c] + cost) // 0/1 (Partition): each item once -> item outer loop, descending sums for each item c: for s = target downto c: dp[s] = dp[s] or dp[s - c] return dp[target]
Template B โ€” the loop direction decides unbounded vs 0/1 knapsack
ONE ITEM (value 2), sums 0..8, starting from reachable = {0} Ascending s = 2..8 item reused (unbounded) T s=0 F s=1 T s=2 F s=3 T s=4 F s=5 T s=6 F s=7 T s=8 Descending s = 8..2 item used once (0/1) T F T F F F F F F Ascending reads dp[s-2] that was ALREADY updated this round, so 2 can stack: 2, 4, 6, 8 all become reachable. Descending reads dp[s-2] from the PREVIOUS round, so 2 is counted at most once: only {0, 2}.
Template C โ€” Grid DP
Movement only right/down means each cell depends on its top and left neighbours. Used by Unique Paths, Minimum Path Sum and Maximal Square (which adds the diagonal).
Pseudocode โ€” Template C (grid, one row of memory)
function grid(m, n): dp = array[n] // dp[c] = answer for (current row, c) for r = 0 to m - 1: for c = 0 to n - 1: up = dp[c] // still holds row r - 1 left = dp[c - 1] // already overwritten with row r dp[c] = combine(up, left) + cell(r, c) // edges: only one neighbour exists return dp[n - 1]
Template C โ€” grid DP, filled row by row
up left dp[r][c] blue = already final (row-major order) dp[r][c] = combine(dp[r-1][c], dp[r][c-1]) + cell Unique Paths: combine = sum Min Path Sum: combine = min Maximal Square: 1 + min(up, left, diag) Row r only reads row r-1 โ†’ keep ONE array: dp[c] is "up", dp[c-1] is "left".
Template D โ€” Two-sequence & interval tables
Two strings โ†’ a table over prefix pairs (Edit Distance, LCS). One string judged on its substrings โ†’ a table over ranges i..j (Longest Palindromic Subsequence).
Pseudocode โ€” Template D (two sequences / interval)
function twoSeq(a, b): // prefixes a[0..i), b[0..j) dp[i][0], dp[0][j] = base cases // empty string vs prefix for i = 1 to m: for j = 1 to n: if a[i-1] == b[j-1]: dp[i][j] = dp[i-1][j-1] + gain else: dp[i][j] = best(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + cost function interval(s): // substring s[i..j] for i = n-1 downto 0: dp[i][i] = base for j = i+1 to n-1: dp[i][j] = from(dp[i+1][j-1], dp[i+1][j], dp[i][j-1]) return dp[0][n-1]
Template D โ€” two-sequence and interval tables
dp[i-1][j-1] dp[i-1][j] dp[i][j-1] dp[i][j] TWO SEQUENCES (prefixes a[0..i), b[0..j)) diag: match / replace up: delete a[i-1] left: insert b[j-1] INTERVAL (substring s[i..j]) dp[i][j-1] dp[i][j] dp[i+1][j-1] dp[i+1][j] row i reads row i+1, so loop i from n-1 DOWN to 0, and j from i+1 up to n-1
Top-down or bottom-up?
  • Top-down (memoised recursion) is faster to write and only visits reachable states โ€” good for sparse state spaces.
  • Bottom-up has no stack-overflow risk (Java's default stack overflows around 104โ€“105 frames) and enables row-rolling.
  • Interview tip: say the recurrence out loud first, write top-down if stuck, then convert to bottom-up once it is correct.
04
Chapter Four ยท Drills 81โ€“82

Linear DP โ€” Climbing Stairs & House Robber

Drill 81 ยท Climbing Stairs
Easy1D linear DPLeetCode 70 โ†—
You are climbing a staircase with n steps. Each move climbs either 1 or 2 steps. In how many distinct ways can you reach the top?
Example
Input: n = 3 Output: 3 // 1+1+1, 1+2, 2+1 Input: n = 5 Output: 8
Key insight: Classify every route by its last move. A route ending in a 1-step came from step n-1; one ending in a 2-step came from step n-2. These two groups never overlap and together cover everything, so ways(n) = ways(n-1) + ways(n-2) โ€” the Fibonacci recurrence.
From brute force to optimal

Brute force recursion climb(n) = climb(n-1) + climb(n-2) is correct but makes about ฯ†n โ‰ˆ 1.618n calls โ€” n = 45 already needs over a billion. Every distinct n is solved once if we fill ways(1), ways(2), ... in increasing order, and because each cell reads only the two before it, two variables replace the whole array.

Walkthrough
Drill 81 โ€” ways to climb, n = 5
ways(i) for n = 5 โ€” each cell = the two cells before it 1 step 1 2 step 2 3 step 3 5 step 4 8 step 5 3 + 5 = 8 Reach step 5 from step 4 (then take a 1-step) or from step 3 (then take a 2-step): ways(5) = ways(4) + ways(3).
iprev2 = ways(i-2)prev1 = ways(i-1)cur = ways(i)
3123
4235
5358
Java โ€” ClimbingStairs.java (complete, runnable)
public class ClimbingStairs { // ways(i) = ways(i - 1) + ways(i - 2): the last move was a 1-step or a 2-step public static int climbStairs(int n) { if (n <= 2) return n; // ways(1) = 1, ways(2) = 2 int prev2 = 1, prev1 = 2; // ways(i - 2), ways(i - 1) for (int i = 3; i <= n; i++) { int cur = prev1 + prev2; prev2 = prev1; prev1 = cur; } return prev1; } public static void main(String[] args) { System.out.println(climbStairs(3)); // 3 System.out.println(climbStairs(5)); // 8 } }
Complexity
ApproachTimeSpaceNotes
Plain recursionO(ฯ†n)O(n) stackExponential; TLE past n โ‰ˆ 40
Memo / tableO(n)O(n)One cell per step
Rolling variablesO(n)O(1)Only prev1 and prev2 are alive
Common bugs & edge cases
  • Base cases: ways(1) = 1, ways(2) = 2 (equivalently ways(0) = 1, ways(1) = 1). Returning n for n <= 2 gives 0 for n = 0 โ€” fine for LeetCode (n >= 1), but state the assumption.
  • Updating in the wrong order (prev1 = cur; prev2 = prev1;) makes both variables equal โ€” always shift the older one first.
  • The answer overflows int at n = 46; LeetCode caps n at 45. Use long if the constraint is larger.
  • Variant: steps of {1, 3, 5} โ†’ ways(i) = ฮฃ ways(i - s); the template is identical, only the window widens.
Drill 82 ยท House Robber
Mediumtake / skip DPLeetCode 198 โ†—
Houses along a street hold nums[i] money. Robbing two adjacent houses triggers the alarm. Return the maximum amount you can rob.
Example
Input: nums = [2, 7, 9, 3, 1] Output: 12 // rob houses 0, 2, 4: 2 + 9 + 1
Key insight: At house i there are exactly two options. Skip it and keep the best of the first i-1 houses, or take it and add it to the best of the first i-2 houses (house i-1 is now forbidden). best(i) = max(best(i-1), best(i-2) + nums[i]).
From brute force to optimal

Trying every subset of non-adjacent houses is O(2n). Greedy choices fail โ€” "always take the largest" picks 9 then cannot combine 7 and 3 in other inputs, and "take every other house" misses patterns like [2, 1, 1, 2] (answer 4 = first + last). The take/skip recurrence looks back only two positions, so two rolling variables give O(n) time and O(1) space.

Walkthrough
Drill 82 โ€” take/skip on [2, 7, 9, 3, 1]
HOUSES best(i) $2 2 take $7 7 take $9 11 take $3 11 skip $1 12 take max(11, 7+3) = 11 max(11, 11+1) = 12 Green houses = one optimal choice, rebuilt backwards: 2 + 9 + 1 = 12 (no two adjacent).
Java โ€” HouseRobber.java (complete, runnable)
public class HouseRobber { // best(i) = max(best(i - 1), best(i - 2) + nums[i]) public static int rob(int[] nums) { int prev2 = 0; // best loot up to house i - 2 int prev1 = 0; // best loot up to house i - 1 for (int x : nums) { int cur = Math.max(prev1, prev2 + x); // skip x, or take x prev2 = prev1; prev1 = cur; } return prev1; } public static void main(String[] args) { System.out.println(rob(new int[]{2, 7, 9, 3, 1})); // 12 System.out.println(rob(new int[]{1, 2, 3, 1})); // 4 } }
Complexity
ApproachTimeSpaceNotes
All subsetsO(2n)O(n)Check adjacency per subset
dp arrayO(n)O(n)best[i] for every prefix
Rolling variablesO(n)O(1)prev2, prev1
Common bugs & edge cases
  • Starting both rolling variables at 0 handles empty input and a single house without special cases.
  • Writing dp[i] = dp[i-2] + nums[i] without the max forgets the "skip" option.
  • Alternating sums (even indices vs odd indices) is a classic wrong answer: [2, 1, 1, 2] โ†’ 3, but the real answer is 4.
  • Follow-up House Robber II (circular street): run the same function on nums[0..n-2] and nums[1..n-1] and take the max.
05
Chapter Five ยท Drills 83โ€“84

Counting & Minimising โ€” Coin Change & Decode Ways

Drill 83 ยท Coin Change
Mediumunbounded knapsackLeetCode 322 โ†—
Given coin denominations coins (unlimited supply of each) and an amount, return the fewest coins that make up exactly that amount, or -1 if it cannot be made.
Example
Input: coins = [1, 2, 5], amount = 11 Output: 3 // 5 + 5 + 1 Input: coins = [2], amount = 3 Output: -1
Key insight: Whatever the optimal pile looks like, it has some last coin c. Remove it and what remains must be an optimal pile for amount - c. So dp[a] = 1 + min over coins c of dp[a - c] โ€” try every possible last coin.
From brute force to optimal

Greedy "largest coin first" works for [1, 2, 5] but fails for coins [1, 3, 4] and amount 6: greedy gives 4+1+1 (3 coins), the optimum is 3+3 (2 coins). Plain recursion over last coins is exponential because amounts repeat. The table has amount + 1 cells, each trying every coin: O(amount ร— coins). Initialise with amount + 1 (more coins than could ever be needed) as a safe "infinity" that cannot overflow when we add 1.

Walkthrough
Drill 83 โ€” coin change table for coins [1, 2, 5], amount 11
dp[a] = 1 + min(dp[a-1], dp[a-2], dp[a-5]) โ€” coin under each cell = the coin that achieved the min a=0 0 base a=1 1 +1 a=2 1 +2 a=3 2 +1 a=4 2 +2 a=5 1 +5 a=6 2 +1 a=7 2 +2 a=8 3 +1 a=9 3 +2 a=10 2 +5 a=11 3 +1 Follow the green arcs back from 11: 11 โ†’ 10 โ†’ 5 โ†’ 0 โ‡’ coins 1, 5, 5 (3 coins).
Java โ€” CoinChange.java (complete, runnable)
import java.util.Arrays; public class CoinChange { // dp[a] = fewest coins that sum to exactly a public static int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; Arrays.fill(dp, amount + 1); // "infinity": more coins than could ever be needed dp[0] = 0; for (int a = 1; a <= amount; a++) { for (int coin : coins) { if (coin <= a) dp[a] = Math.min(dp[a], dp[a - coin] + 1); } } return dp[amount] > amount ? -1 : dp[amount]; } public static void main(String[] args) { System.out.println(coinChange(new int[]{1, 2, 5}, 11)); // 3 System.out.println(coinChange(new int[]{2}, 3)); // -1 System.out.println(coinChange(new int[]{1}, 0)); // 0 } }
Complexity
ApproachTimeSpaceNotes
Recursion over last coinO(kamount)O(amount)k = number of coins
Greedy largest-firstO(k log k)O(1)WRONG for non-canonical coin systems
Bottom-up DPO(amount ร— k)O(amount)Each amount tries every coin
Common bugs & edge cases
  • Original drill code called Arrays.fill without import java.util.Arrays โ€” it did not compile.
  • Using Integer.MAX_VALUE as infinity overflows on dp[a - c] + 1 and turns negative; use amount + 1.
  • amount = 0 must return 0 (zero coins), not -1.
  • Do not confuse with Coin Change II (count combinations): there the coin loop must be OUTER to avoid counting 1+2 and 2+1 twice.
Drill 84 ยท Decode Ways
Medium1D linear DPLeetCode 91 โ†—
Letters are encoded as A โ†’ 1 ... Z โ†’ 26. Given a digit string s, return how many ways it can be decoded. A code may not have a leading zero ("06" is invalid).
Example
Input: s = "226" Output: 3 // "BBF" (2 2 6), "BZ" (2 26), "VF" (22 6) Input: s = "06" Output: 0
Key insight: It is Climbing Stairs with rules on the steps. The last piece of a decoding is either one digit (valid if 1โ€“9) or two digits (valid if 10โ€“26). dp[i] = (s[i-1] valid ? dp[i-1] : 0) + (s[i-2..i) valid ? dp[i-2] : 0).
From brute force to optimal

Generating all decodings recursively is exponential (a string of 1s has Fibonacci-many decodings). Index the DP by prefix length so that dp[0] = 1 (the empty prefix has one, empty, decoding) โ€” that base case makes the two-digit transition at i = 2 work without special code. Zeros are where bugs live: "0" alone is never valid, "10" and "20" are the only ways a zero can be used.

Walkthrough
Drill 84 โ€” decode ways for "226"
Digits sit BETWEEN the prefix boundaries; dp[i] counts decodings of the first i digits 2 2 6 1 dp[0] 1 dp[1] 2 dp[2] 3 dp[3] "2" "2" "6" "22" "26" blue arcs: one-digit step (1..9) | gold arcs: two-digit step (10..26) dp[3] = dp[2] + dp[1] = 2 + 1 = 3
ione digittwo digitsdp[i]
1"2" valid โ†’ +dp[0] = 1โ€”1
2"2" valid โ†’ +dp[1] = 1"22" valid โ†’ +dp[0] = 12
3"6" valid โ†’ +dp[2] = 2"26" valid โ†’ +dp[1] = 13
Java โ€” DecodeWays.java (complete, runnable)
public class DecodeWays { // dp[i] = number of ways to decode the prefix s[0..i) public static int numDecodings(String s) { int n = s.length(); if (n == 0) return 0; int[] dp = new int[n + 1]; dp[0] = 1; // empty prefix: exactly one (empty) decoding for (int i = 1; i <= n; i++) { char c = s.charAt(i - 1); if (c != '0') dp[i] += dp[i - 1]; // single digit 1..9 if (i >= 2) { int two = (s.charAt(i - 2) - '0') * 10 + (c - '0'); if (two >= 10 && two <= 26) dp[i] += dp[i - 2]; // pair 10..26 } } return dp[n]; } public static void main(String[] args) { System.out.println(numDecodings("226")); // 3 (2 2 6, 2 26, 22 6) System.out.println(numDecodings("06")); // 0 (leading zero) System.out.println(numDecodings("11106")); // 2 (1 1 10 6, 11 10 6) } }
Complexity
ApproachTimeSpaceNotes
Enumerate decodingsO(2n)O(n)Fibonacci-many for "1111..."
dp arrayO(n)O(n)Shown above โ€” easiest to reason about
Rolling variablesO(n)O(1)Keep only dp[i-1], dp[i-2]
Common bugs & edge cases
  • The original listed the decodings of "226" as "BBF", "VF", "BF" โ€” "BF" would be 2 6. The third decoding is "BZ" (2 26).
  • The original read s.charAt(0) unguarded and crashed on an empty string; the fixed version returns 0.
  • Treating "0" as a valid single digit, or "05" as a valid pair (5 < 10), inflates the count.
  • Once dp[i] becomes 0 (e.g. "100" at the second 0) every later value is 0 โ€” that is correct, not a bug.
06
Chapter Six ยท Drills 85โ€“86

Scanning Back โ€” Word Break & LIS

Drill 85 ยท Word Break
Mediumprefix DPLeetCode 139 โ†—
Given a string s and a dictionary wordDict, return true if s can be split into a sequence of one or more dictionary words (words may be reused).
Example
Input: s = "leetcode", wordDict = ["leet", "code"] Output: true // "leet" + "code" Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] Output: false
Key insight: A prefix is breakable if it ends with a dictionary word whose start is itself a breakable boundary. dp[i] = OR over j < i of (dp[j] AND s[j..i) in dict). It is unbounded knapsack where the "coins" are words.
From brute force to optimal

Recursive splitting re-checks the same suffix many times โ€” "aaaa...ab" with words {"a", "aa", "aaa"} is exponential. With the boolean table each boundary is decided once. Two refinements: store the dictionary in a HashSet for O(1) membership, and only look back as far as the longest word, which caps the inner loop at L instead of n.

Walkthrough
Drill 85 โ€” word break on "leetcode"
dp[i] lives on the boundary BEFORE character i; an arc j โ†’ i means s[j..i) is a word and dp[j] was true l e e t c o d e 0 T 1 F 2 F 3 F 4 T 5 F 6 F 7 F 8 T leet code boundary dp dp[4] = dp[0] and "leet" in dict; dp[8] = dp[4] and "code" in dict โ‡’ true.
Java โ€” WordBreak.java (complete, runnable)
import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; public class WordBreak { // dp[i] = true if the prefix s[0..i) splits into dictionary words public static boolean wordBreak(String s, List<String> wordDict) { Set<String> dict = new HashSet<>(wordDict); int maxLen = 0; for (String w : dict) maxLen = Math.max(maxLen, w.length()); int n = s.length(); boolean[] dp = new boolean[n + 1]; dp[0] = true; // empty prefix is trivially breakable for (int i = 1; i <= n; i++) { // only look back as far as the longest dictionary word for (int j = i - 1; j >= Math.max(0, i - maxLen); j--) { if (dp[j] && dict.contains(s.substring(j, i))) { dp[i] = true; break; } } } return dp[n]; } public static void main(String[] args) { System.out.println(wordBreak("leetcode", Arrays.asList("leet", "code"))); // true System.out.println(wordBreak("catsandog", Arrays.asList("cats", "dog", "sand", "and", "cat"))); // false } }
Complexity
ApproachTimeSpaceNotes
Recursive splittingO(2n)O(n)Every split point tried
dp, all jO(n3)O(n + dict)n2 pairs ร— O(n) substring/hash
dp, j within maxLenO(n ยท L2)O(n + dict)L = longest word; substring costs O(L)
Common bugs & edge cases
  • The original claimed O(n2); substring plus hashing costs O(length), so the honest bound is O(n3) (or O(nยทL2) with the maxLen cap).
  • Forgetting dp[0] = true makes every dp value false.
  • Breaking out of the inner loop as soon as dp[i] is true is safe and saves time; continuing is just wasted work.
  • Word Break II (return all sentences) is backtracking + memo โ€” the boolean table here can prune it.
Drill 86 ยท Longest Increasing Subsequence
Medium"ending at i" DP + binary searchLeetCode 300 โ†—
Given an integer array nums, return the length of the longest strictly increasing subsequence (elements keep their order but need not be contiguous).
Example
Input: nums = [10, 9, 2, 5, 3, 7, 101, 18] Output: 4 // e.g. [2, 3, 7, 101] or [2, 3, 7, 18]
Key insight: Quadratic DP: dp[i] = 1 + max dp[j] over j < i with nums[j] < nums[i] โ€” the best chain that can be extended by nums[i]. Faster: for each length k remember only the smallest tail value any increasing subsequence of that length can have. A smaller tail is never worse, and the tails array stays sorted, so binary search applies.
From brute force to optimal

All 2n subsequences is hopeless. The O(n2) DP is the version to derive first in an interview. The patience-sorting version processes each x with one binary search: find the first tail โ‰ฅ x; if none, x extends the longest chain, otherwise x replaces that tail (same length, smaller ending โ€” strictly better for the future). Using "โ‰ฅ" (lower bound) is what makes the subsequence strictly increasing.

Walkthrough
Drill 86 โ€” patience-sorting tails on [10, 9, 2, 5, 3, 7, 101, 18]
x tails after x (smallest possible tail for each length) action 10 10 extend at index 0 9 9 replace at index 0 2 2 replace at index 0 5 2 5 extend at index 1 3 2 3 replace at index 1 7 2 3 7 extend at index 2 101 2 3 7 101 extend at index 3 18 2 3 7 18 replace at index 3 Final size = 4 = LIS length. (tails itself is NOT an LIS โ€” it only tracks the best tail per length.)
Java โ€” LongestIncreasingSubsequence.java (complete, runnable)
public class LongestIncreasingSubsequence { // O(n^2): dp[i] = length of the longest increasing subsequence ending at i public static int lengthOfLISQuadratic(int[] nums) { int[] dp = new int[nums.length]; int best = 0; for (int i = 0; i < nums.length; i++) { dp[i] = 1; for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } best = Math.max(best, dp[i]); } return best; } // O(n log n): tails[k] = smallest tail of any increasing subsequence of length k + 1 public static int lengthOfLIS(int[] nums) { int[] tails = new int[nums.length]; int size = 0; for (int x : nums) { int lo = 0, hi = size; // first index with tails[idx] >= x while (lo < hi) { int mid = lo + (hi - lo) / 2; if (tails[mid] < x) lo = mid + 1; else hi = mid; } tails[lo] = x; // extend (lo == size) or replace if (lo == size) size++; } return size; } public static void main(String[] args) { int[] nums = {10, 9, 2, 5, 3, 7, 101, 18}; System.out.println(lengthOfLISQuadratic(nums)); // 4 System.out.println(lengthOfLIS(nums)); // 4 (e.g. 2, 3, 7, 18) System.out.println(lengthOfLIS(new int[]{7, 7, 7})); // 1 (strictly increasing) } }
Complexity
ApproachTimeSpaceNotes
All subsequencesO(2n ยท n)O(n)Hopeless past n โ‰ˆ 25
dp ending at iO(n2)O(n)Easy to extend to "reconstruct the LIS"
Tails + binary searchO(n log n)O(n)Length only; tails is not itself a valid LIS
Common bugs & edge cases
  • Using upper bound (first tail > x) computes the longest NON-decreasing subsequence: [7, 7, 7] would return 3 instead of 1.
  • Returning dp[n-1] in the quadratic version is wrong โ€” the LIS may end anywhere; take the max over all i.
  • Printing tails as "the LIS" is a common misconception: after [2, 5, 3] the tails are [2, 3] even though 5 came first.
  • To reconstruct the actual subsequence in O(n log n), store for each element the index of its predecessor when it is placed.
07
Chapter Seven ยท Drills 87โ€“88

State Tricks โ€” Max Product & Jump Game

Drill 87 ยท Maximum Product Subarray
MediumKadane with max & minLeetCode 152 โ†—
Given an integer array nums, find the contiguous non-empty subarray with the largest product and return that product.
Example
Input: nums = [2, 3, -2, 4] Output: 6 // [2, 3] Input: nums = [-2, 3, -4] Output: 24 // the whole array
Key insight: For sums (Kadane) the best subarray ending at i only needs the best ending at i-1. For products a negative number turns the smallest (most negative) product into the largest. So carry both maxEnd and minEnd; the new extremes are among x, maxEnd ยท x, minEnd ยท x.
From brute force to optimal

Checking every subarray is O(n2) with a running product. A single running max fails on [-2, 3, -4]: after 3 it would forget the -6 that becomes 24. Tracking the pair of extremes keeps the state complete in O(1) per element. Zeros need no special case: the candidate x = 0 resets both extremes, and the next element starts fresh.

Walkthrough
xcandidates (x, maxยทx, minยทx)maxEndminEndbestwhy
--2-2-2-2start: everything = nums[0]
33, -6, -63-63-6 is kept as minEnd: it may become huge later
-4-4, -12, 2424-1224negative x flips the old min (-6) into the new max 24
Java โ€” MaxProductSubarray.java (complete, runnable)
public class MaxProductSubarray { // Track both the largest and the smallest product ending at i: // a negative number turns the smallest into the largest. public static int maxProduct(int[] nums) { int maxEnd = nums[0], minEnd = nums[0], best = nums[0]; for (int i = 1; i < nums.length; i++) { int x = nums[i]; int a = maxEnd * x, b = minEnd * x; maxEnd = Math.max(x, Math.max(a, b)); // start fresh, or extend minEnd = Math.min(x, Math.min(a, b)); best = Math.max(best, maxEnd); } return best; } public static void main(String[] args) { System.out.println(maxProduct(new int[]{2, 3, -2, 4})); // 6 System.out.println(maxProduct(new int[]{-2, 3, -4})); // 24 System.out.println(maxProduct(new int[]{-2, 0, -1})); // 0 } }
Complexity
ApproachTimeSpaceNotes
All subarraysO(n2)O(1)Running product per start
Max/min KadaneO(n)O(1)Two extremes per position
Common bugs & edge cases
  • Updating maxEnd before computing minEnd uses the NEW max in the min formula โ€” compute both products from the old values first (the code stores them in a, b).
  • Initialising best to 0 is wrong for [-3] (answer -3); initialise with nums[0].
  • Products can overflow int for long arrays of large values; LeetCode guarantees the answer fits in 32 bits, otherwise use long.
  • An alternative O(n) proof: the answer is a prefix product or suffix product between zeros โ€” scan both directions and reset at 0.
Drill 88 ยท Jump Game
MediumDP โ†’ greedy reachLeetCode 55 โ†—
nums[i] is the maximum jump length from index i. Starting at index 0, return true if you can reach the last index.
Example
Input: nums = [2, 3, 1, 1, 4] Output: true // 0 โ†’ 1 โ†’ 4 Input: nums = [3, 2, 1, 0, 4] Output: false // every route lands on the 0 at index 3
Key insight: The DP "good[i] = can index i reach the end" works, but notice the reachable indices always form a contiguous prefix [0, reach]. So the whole table collapses into one number: the furthest index reachable so far.
From brute force to optimal

Backtracking over every jump length is exponential; the boolean DP is O(n2) because each index tries all its jump lengths. The greedy scan visits each index once: if the current index is beyond reach, it can never be visited, so return false; otherwise extend reach with i + nums[i]. This is the "greedy collapse" end of the DP spectrum โ€” the DP argument is what proves the greedy correct.

Walkthrough
Drill 88 โ€” reach frontier on both examples
[2,3,1,1,4] 2 i=0 reach=2 3 i=1 reach=4 1 i=2 1 i=3 4 i=4 reach 4 >= 4 โ‡’ true [3,2,1,0,4] 3 i=0 reach=3 2 i=1 reach=3 1 i=2 reach=3 0 i=3 reach=3 4 i=4 i=4 > reach=3 stuck at the 0 โ‡’ false reach = furthest index any visited cell can jump to. If the scan index ever passes reach, nothing can carry us forward.
Java โ€” JumpGame.java (complete, runnable)
public class JumpGame { // Greedy collapse of the DP: keep only the furthest index reachable so far public static boolean canJump(int[] nums) { int reach = 0; for (int i = 0; i < nums.length; i++) { if (i > reach) return false; // index i is unreachable reach = Math.max(reach, i + nums[i]); if (reach >= nums.length - 1) return true; } return true; } public static void main(String[] args) { System.out.println(canJump(new int[]{2, 3, 1, 1, 4})); // true System.out.println(canJump(new int[]{3, 2, 1, 0, 4})); // false System.out.println(canJump(new int[]{0})); // true (already there) } }
Complexity
ApproachTimeSpaceNotes
BacktrackingO(2n)O(n)Tries every jump length
Boolean DPO(n2)O(n)good[i] from good[i+1..i+nums[i]]
Greedy reachO(n)O(1)Single forward pass
Common bugs & edge cases
  • Checking i > reach AFTER updating reach lets an unreachable index extend the frontier.
  • A single-element array is already at the goal โ€” return true even if nums[0] = 0.
  • Jump Game II (minimum jumps) is a different greedy: BFS-like levels over [start, end] windows.
  • Equivalent backward greedy: walk from the end keeping the leftmost "good" index; answer is whether it reaches 0.
08
Chapter Eight ยท Drills 89โ€“90

Grid DP โ€” Unique Paths & Minimum Path Sum

Drill 89 ยท Unique Paths
Medium2D grid DPLeetCode 62 โ†—
A robot starts at the top-left of an m ร— n grid and can only move right or down. How many distinct paths reach the bottom-right corner?
Example
Input: m = 3, n = 7 Output: 28 Input: m = 3, n = 2 Output: 3 // DDR, DRD, RDD
Key insight: Every path into cell (r, c) arrives from above or from the left โ€” never both โ€” so paths(r, c) = paths(r-1, c) + paths(r, c-1), with 1 along the top row and left column.
From brute force to optimal

Enumerating paths is exponential: there are C(m+n-2, m-1) of them. The grid table fills in O(mn). Since row r only reads row r-1, a single array of length n suffices: before the update dp[c] still holds "above", and dp[c-1] already holds "left". (A pure combinatorics answer also exists: choose which m-1 of the m+n-2 moves are downs.)

Walkthrough
Drill 89 โ€” unique paths on a 3 x 7 grid
1 1 1 1 1 1 1 1 2 3 4 5 6 7 1 3 6 10 15 21 28 r0 r1 r2 START top-left FINISH = 28 Gold cells feed the blue one: 4 (from above) + 6 (from the left) = 10. Grey edge cells are 1: only one straight route.
Java โ€” UniquePaths.java (complete, runnable)
import java.util.Arrays; public class UniquePaths { // paths(r, c) = paths(r - 1, c) + paths(r, c - 1); one row kept at a time public static int uniquePaths(int m, int n) { int[] dp = new int[n]; Arrays.fill(dp, 1); // first row: one way to each cell for (int r = 1; r < m; r++) { for (int c = 1; c < n; c++) { dp[c] += dp[c - 1]; // dp[c] (above) + dp[c - 1] (left) } } return dp[n - 1]; } public static void main(String[] args) { System.out.println(uniquePaths(3, 7)); // 28 System.out.println(uniquePaths(3, 2)); // 3 (DDR, DRD, RDD) } }
Complexity
ApproachTimeSpaceNotes
Enumerate pathsO(2m+n)O(m+n)Recursion right/down
2D tableO(mn)O(mn)Clear, easy to debug
One rowO(mn)O(n)Shown in the code
Binomial C(m+n-2, m-1)O(min(m, n))O(1)Use long and divide as you multiply
Common bugs & edge cases
  • The original used Arrays.fill without import java.util.Arrays, and listed the paths for m = 3, n = 2 as "RRD, RDR, DRR" โ€” that is 2 rights in a 2-column grid; the real paths are DDR, DRD, RDD.
  • Mixing up m (rows) and n (columns) gives the right count only because the problem is symmetric โ€” it bites in the obstacle variant.
  • Unique Paths II (obstacles): set dp[c] = 0 on an obstacle and do not pre-fill the first row with 1s past an obstacle.
Drill 90 ยท Minimum Path Sum
Medium2D grid DPLeetCode 64 โ†—
Given an m ร— n grid of non-negative costs, find a path from top-left to bottom-right (moving only right or down) that minimises the sum of the cells on it.
Example
Input: grid = [[1, 3, 1], [1, 5, 1], [4, 2, 1]] Output: 7 // 1 โ†’ 3 โ†’ 1 โ†’ 1 โ†’ 1
Key insight: Same shape as Unique Paths with min instead of +: the cheapest way into a cell is its own cost plus the cheaper of the two ways in. Greedy "step to the cheaper neighbour" fails because a cheap step can lead into an expensive region.
From brute force to optimal

Brute force tries every monotone path (exponential). The table dp[r][c] = grid[r][c] + min(dp[r-1][c], dp[r][c-1]) is O(mn); edges have only one predecessor. The original drill wrote the sums back into grid to get O(1) extra space โ€” fast, but it destroys the caller's input. The version below keeps one row instead.

Walkthrough
Drill 90 โ€” minimum path sum, input vs dp
INPUT grid (cost per cell) 1 3 1 1 5 1 4 2 1 dp = cheapest cost to reach cell 1 4 5 2 7 6 6 8 7 dp[1][1] = min(up 4, left 2) + 5 = 7. Green = optimal path 1 โ†’ 3 โ†’ 1 โ†’ 1 โ†’ 1, total 7.
Java โ€” MinPathSum.java (complete, runnable)
public class MinPathSum { // dp[c] holds the cheapest cost to reach (row, c); the input grid is not modified public static int minPathSum(int[][] grid) { int m = grid.length, n = grid[0].length; int[] dp = new int[n]; for (int r = 0; r < m; r++) { for (int c = 0; c < n; c++) { if (r == 0 && c == 0) dp[c] = grid[0][0]; else if (r == 0) dp[c] = dp[c - 1] + grid[r][c]; // only from left else if (c == 0) dp[c] = dp[c] + grid[r][c]; // only from above else dp[c] = Math.min(dp[c], dp[c - 1]) + grid[r][c]; } } return dp[n - 1]; } public static void main(String[] args) { int[][] grid = {{1, 3, 1}, {1, 5, 1}, {4, 2, 1}}; System.out.println(minPathSum(grid)); // 7 (1 -> 3 -> 1 -> 1 -> 1) } }
Complexity
ApproachTimeSpaceNotes
All pathsO(2m+n)O(m+n)Exponential
In-place on gridO(mn)O(1)Mutates the input (original drill)
One row dpO(mn)O(n)Input untouched
Common bugs & edge cases
  • Mutating the input array is a side effect interviewers will ask about โ€” say it explicitly if you choose it.
  • Taking min over an out-of-range neighbour at the edges: handle row 0 and column 0 separately (or pad with +โˆž).
  • Greedy local choice fails: on [[1, 2, 9], [5, 9, 9], [1, 1, 1]] it steps right to the 2 and pays 14, while going down first costs only 9.
  • To print the path, keep the full 2D table and walk back from the end choosing the smaller predecessor (green cells in the diagram).
09
Chapter Nine ยท Drills 91โ€“92

Grid & Knapsack โ€” Maximal Square & Partition

Drill 91 ยท Maximal Square
Medium2D grid DP (3 neighbours)LeetCode 221 โ†—
Given an m ร— n binary matrix of '0' and '1' characters, find the largest square containing only 1s and return its area.
Example
Input: matrix = [["1","0","1","0","0"], ["1","0","1","1","1"], ["1","1","1","1","1"], ["1","0","0","1","0"]] Output: 4 // a 2 x 2 square of 1s
Key insight: Let dp[r][c] be the side of the largest all-1 square whose bottom-right corner is (r, c). It can only be as big as the weakest of its three neighbours allows: 1 + min(up, left, up-left). One missing 1 in any of those directions caps the square.
From brute force to optimal

Checking every top-left corner and every size is O((mn)2) or worse. The recurrence makes each cell O(1). Padding the table with an extra zero row and column removes all edge checks. Track the maximum side while filling and square it at the end โ€” the question asks for area.

Walkthrough
Drill 91 โ€” maximal square, matrix vs dp
MATRIX 1 0 1 0 0 1 0 1 1 1 1 1 1 1 1 1 0 0 1 0 dp (side of square ending here) 1 0 1 0 0 1 0 1 1 1 1 1 1 2 2 1 0 0 1 0 dp[2][3] = 1 + min(up 1, left 1, diag 1) = 2 โ†’ the green 2 x 2 block. Largest side = 2, area = 4.
Java โ€” MaximalSquare.java (complete, runnable)
public class MaximalSquare { // dp[r][c] = side of the largest all-'1' square whose bottom-right corner is (r-1, c-1) public static int maximalSquare(char[][] matrix) { if (matrix.length == 0 || matrix[0].length == 0) return 0; int m = matrix.length, n = matrix[0].length, maxSide = 0; int[][] dp = new int[m + 1][n + 1]; // padded row/col of zeros for (int r = 1; r <= m; r++) { for (int c = 1; c <= n; c++) { if (matrix[r - 1][c - 1] == '1') { dp[r][c] = 1 + Math.min(dp[r - 1][c - 1], Math.min(dp[r - 1][c], dp[r][c - 1])); maxSide = Math.max(maxSide, dp[r][c]); } } } return maxSide * maxSide; // area, not side } public static void main(String[] args) { char[][] matrix = { {'1', '0', '1', '0', '0'}, {'1', '0', '1', '1', '1'}, {'1', '1', '1', '1', '1'}, {'1', '0', '0', '1', '0'} }; System.out.println(maximalSquare(matrix)); // 4 } }
Complexity
ApproachTimeSpaceNotes
Every corner ร— every sizeO((mn)2)O(1)Re-scans cells repeatedly
Padded 2D dpO(mn)O(mn)Shown in the code
One row + saved diagonalO(mn)O(n)Keep the old dp[c-1] of the previous row in a temp
Common bugs & edge cases
  • Returning the side instead of the area (or vice versa) โ€” read the question.
  • Comparing matrix[r][c] == 1 (int) on a char[][] is always false; compare with '1'.
  • The original crashed on an empty matrix by reading matrix[0]; the fixed version returns 0.
  • Using only up and left (no diagonal) accepts L-shapes as squares.
Drill 92 ยท Partition Equal Subset Sum
Medium0/1 knapsackLeetCode 416 โ†—
Given an array of positive integers, decide whether it can be split into two subsets with equal sums.
Example
Input: nums = [1, 5, 11, 5] Output: true // {11} and {1, 5, 5} Input: nums = [1, 2, 3, 5] Output: false // total 11 is odd
Key insight: Two equal halves means one subset sums to exactly total / 2 โ€” the other half is automatically the rest. So the question becomes "is target = total / 2 reachable?", a 0/1 knapsack on a boolean array of sums.
From brute force to optimal

Trying all 2n subsets is the brute force. The DP keeps dp[s] = "some subset of the items seen so far sums to s". For each item, sweep s from target down to the item value so dp[s - x] still refers to the previous round โ€” the item is used at most once (Template B). An odd total is rejected immediately.

Walkthrough
Drill 92 โ€” reachable subset sums, target = 11
0 1 2 3 4 5 6 7 8 9 10 11 start T . . . . . . . . . . . after 1 T T . . . . . . . . . . after 5 T T . . . T T . . . . . after 11 T T . . . T T . . . . T after 5 T T . . . T T . . . T T Green = newly reachable sums in that round. Sum 11 becomes reachable after 11 itself โ†’ true ({11} vs {1, 5, 5}).
Java โ€” PartitionEqualSubsetSum.java (complete, runnable)
public class PartitionEqualSubsetSum { // 0/1 knapsack: can some subset sum to total / 2? public static boolean canPartition(int[] nums) { int total = 0; for (int x : nums) total += x; if (total % 2 != 0) return false; // odd total can never split evenly int target = total / 2; boolean[] dp = new boolean[target + 1]; // dp[s]: some subset sums to s dp[0] = true; for (int x : nums) { for (int s = target; s >= x; s--) { // right-to-left: each x used once dp[s] = dp[s] || dp[s - x]; } } return dp[target]; } public static void main(String[] args) { System.out.println(canPartition(new int[]{1, 5, 11, 5})); // true ({11} and {1, 5, 5}) System.out.println(canPartition(new int[]{1, 2, 3, 5})); // false (total 11 is odd) } }
Complexity
ApproachTimeSpaceNotes
All subsetsO(2n)O(n)Include / exclude recursion
2D dp[i][s]O(n ยท target)O(n ยท target)Classic knapsack table
1D dp, descending sO(n ยท target)O(target)Pseudo-polynomial: depends on the sum, not just n
Common bugs & edge cases
  • Iterating s upwards silently turns this into the unbounded version: [1, 2, 5] would "reach" 4 via 2 + 2.
  • Forgetting the odd-total check works but wastes a full DP pass.
  • Pseudo-polynomial: with sums up to 109 this table is infeasible โ€” then use meet-in-the-middle or a bitset.
  • Java bitset trick: BitSet or long[] with bits |= bits << x does the same sweep 64 sums at a time.
10
Chapter Ten ยท Drills 93โ€“94

String Tables โ€” Palindromic Subsequence & Edit Distance

Drill 93 ยท Longest Palindromic Subsequence
Mediuminterval DPLeetCode 516 โ†—
Given a string s, return the length of its longest subsequence that reads the same forwards and backwards.
Example
Input: s = "bbbab" Output: 4 // "bbbb" Input: s = "cbbd" Output: 2 // "bb"
Key insight: Look at the two ends of s[i..j]. If they match, they can wrap whatever is best inside: dp[i+1][j-1] + 2. If they do not, at least one end is unused, so drop one: max(dp[i+1][j], dp[i][j-1]).
From brute force to optimal

Brute force checks 2n subsequences. The interval table has n2/2 cells, each O(1). The fill order matters: dp[i][j] reads row i+1, so i must go from n-1 down to 0. The original drill instead computed the LCS of s and reverse(s) โ€” also correct (an LPS is exactly a common subsequence of a string and its reverse) and also O(n2), but it builds an (n+1)2 table and hides the interval idea that generalises to other problems.

Walkthrough
Drill 93 โ€” interval table for "bbbab" (upper triangle only)
j=0 b j=1 b j=2 b j=3 a j=4 b i=0 b 1 2 3 3 4 i=1 b 1 2 2 3 i=2 b 1 1 3 i=3 a 1 1 i=4 b 1 diagonal = 1 (single chars) s[0] = s[4] = b, so dp[0][4] = dp[1][3] + 2 = 2 + 2 = 4 dp[1][3] = "bba" โ†’ "bb" = 2 fill order: i = 4 down to 0, j = i + 1 up to 4
Java โ€” LongestPalindromicSubsequence.java (complete, runnable)
public class LongestPalindromicSubsequence { // dp[i][j] = longest palindromic subsequence inside s[i..j] public static int longestPalindromeSubseq(String s) { int n = s.length(); if (n == 0) return 0; int[][] dp = new int[n][n]; for (int i = n - 1; i >= 0; i--) { // i descends: dp[i + 1][..] is ready dp[i][i] = 1; for (int j = i + 1; j < n; j++) { if (s.charAt(i) == s.charAt(j)) dp[i][j] = dp[i + 1][j - 1] + 2; else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); } } return dp[0][n - 1]; } public static void main(String[] args) { System.out.println(longestPalindromeSubseq("bbbab")); // 4 ("bbbb") System.out.println(longestPalindromeSubseq("cbbd")); // 2 ("bb") } }
Complexity
ApproachTimeSpaceNotes
All subsequencesO(2n ยท n)O(n)Check each for palindrome
LCS(s, reverse(s))O(n2)O(n2)Original drill approach
Interval dp[i][j]O(n2)O(n2)Reducible to O(n) with two rows
Common bugs & edge cases
  • Looping i upwards reads dp[i+1][...] before it is filled โ€” all zeros, wrong answers.
  • Do not confuse with Longest Palindromic SUBSTRING (contiguous, drill 28): there you expand around centres.
  • For j = i + 1 with equal chars, dp[i+1][j-1] is a lower-triangle cell (value 0) โ€” correct, giving 2.
  • Empty string: guard n = 0 before indexing dp[0][n-1], which would otherwise read column -1.
Drill 94 ยท Edit Distance
Mediumtwo-sequence DPLeetCode 72 โ†—
Given two words, return the minimum number of single-character operations โ€” insert, delete or replace โ€” needed to convert word1 into word2.
Example
Input: word1 = "horse", word2 = "ros" Output: 3 // horse โ†’ rorse (replace h) โ†’ rose (delete r) โ†’ ros (delete e) Input: word1 = "intention", word2 = "execution" Output: 5
Key insight: Compare the last characters of the two prefixes. Equal โ†’ free, solve the shorter prefixes. Different โ†’ pay 1 for one of three moves: replace (diag), delete from word1 (up), or insert into word1 (left), and take the cheapest.
From brute force to optimal

The recursive version branches three ways per mismatch โ€” O(3m+n). The table over prefix pairs has (m+1)(n+1) cells: row 0 means "word1 is empty" (insert j chars) and column 0 means "word2 is empty" (delete i chars). Each cell is O(1), and walking back through the table recovers an actual edit script (green cells).

Walkthrough
Drill 94 โ€” edit distance table, "horse" -> "ros"
"" r o s "" 0 1 2 3 h 1 1 2 3 o 2 2 1 2 r 3 2 2 2 s 4 3 3 2 e 5 4 4 3 Green = one optimal backtrace from dp[5][3] = 3 1. replace hโ†’r 2. delete r 3. delete e horse โ†’ rorse โ†’ rose โ†’ ros match (diagonal, same letter) costs 0 every other move costs 1
Java โ€” EditDistance.java (complete, runnable)
public class EditDistance { // dp[i][j] = edits to turn word1[0..i) into word2[0..j) public static int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i <= m; i++) dp[i][0] = i; // delete everything for (int j = 0; j <= n; j++) dp[0][j] = j; // insert everything for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; // free match } else { dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], // replace Math.min(dp[i - 1][j], // delete dp[i][j - 1])); // insert } } } return dp[m][n]; } public static void main(String[] args) { System.out.println(minDistance("horse", "ros")); // 3 System.out.println(minDistance("intention", "execution")); // 5 } }
Complexity
ApproachTimeSpaceNotes
RecursionO(3m+n)O(m+n)Three branches per mismatch
Full tableO(mn)O(mn)Needed to reconstruct the edit script
Two rowsO(mn)O(min(m, n))Length only; keep the diagonal in a temp
Common bugs & edge cases
  • Forgetting the base row/column (dp[i][0] = i, dp[0][j] = j) treats converting to/from "" as free.
  • Adding 1 even when characters match (the match must be free: dp[i-1][j-1]).
  • Mixing up which neighbour is insert vs delete does not change the number, but it does produce a wrong edit script.
  • Related: LCS uses the same table with +1 on match and max otherwise; one-edit-distance can be checked in O(n) without a table.
11
Chapter Eleven ยท Summary

All Fourteen Drills at a Glance

Every drill on this page reduces to the same four answers โ€” state, recurrence, order, memory. The table lists them side by side; if you can reproduce the "pattern" column from the problem statement alone, the code follows.

#DrillPatternTimeSpaceLevel
81Climbing StairsLinear DP (Fibonacci)O(n)O(1)Easy
82House RobberLinear take/skipO(n)O(1)Medium
83Coin ChangeUnbounded knapsackO(amount ยท k)O(amount)Medium
84Decode WaysLinear DP with validityO(n)O(n) โ†’ O(1)Medium
85Word BreakPrefix DP + HashSetO(n ยท L2)O(n)Medium
86Longest Increasing SubsequenceTails + binary searchO(n log n)O(n)Medium
87Maximum Product SubarrayKadane with max & minO(n)O(1)Medium
88Jump GameGreedy reach (collapsed DP)O(n)O(1)Medium
89Unique PathsGrid DP, one rowO(mn)O(n)Medium
90Minimum Path SumGrid DP, one rowO(mn)O(n)Medium
91Maximal SquareGrid DP, 3 neighboursO(mn)O(mn)Medium
92Partition Equal Subset Sum0/1 knapsack (descending)O(n ยท sum)O(sum)Medium
93Longest Palindromic SubsequenceInterval DPO(n2)O(n2)Medium
94Edit DistanceTwo-sequence DPO(mn)O(mn)Medium
Patterns to carry forward:
  • Last decision โ€” every recurrence on this page came from asking "what was the last step / coin / character?"
  • Loop direction is semantics โ€” ascending vs descending sums is the difference between unbounded and 0/1 knapsack.
  • Extra state beats a wrong state โ€” Max Product needed both max and min; LIS needed tails per length.
  • Squeeze last โ€” write the full table first, then roll rows once the answer is right.
What to practice next

โ† 08 ยท Graphs All drills 10 ยท Backtracking & Design โ†’