diff --git a/README.md b/README.md
index 1224978..6e4956b 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ One module per article. This repository currently holds:
| [`lru-cache`](lru-cache/) | LRU Cache in Java: LinkedHashMap, HashMap + Doubly Linked List and a Thread-Safe Version | published |
| [`binary-tree-traversals`](binary-tree-traversals/) | Binary Tree Traversals in Java: Inorder, Preorder, Postorder and Level Order (Recursive and Iterative) | this commit |
| [`sliding-window`](sliding-window/) | Sliding Window and Two Pointers in Java: 12 Interview Problems Solved | this commit |
+| [`dynamic-programming`](dynamic-programming/) | Dynamic Programming in Java for Beginners: Memoization vs Tabulation (10 Classic Problems) | this commit |
## `collections` module
diff --git a/dynamic-programming/README.md b/dynamic-programming/README.md
new file mode 100644
index 0000000..ff052d1
--- /dev/null
+++ b/dynamic-programming/README.md
@@ -0,0 +1,38 @@
+# dynamic-programming
+
+Runnable companion for the ankurm.com article
+[Dynamic Programming in Java for Beginners: Memoization vs Tabulation (10 Classic Problems)](https://ankurm.com/dynamic-programming-java-memoization-vs-tabulation/).
+
+Ten classic problems, each solved top-down (memoization), bottom-up (tabulation) and with a space-optimised
+table. Tests cross-check every variant on random inputs, and write the transcripts the article quotes.
+There is no `docs/` folder on purpose: the explanations live in the article.
+
+| Component | Version | Verified against |
+|---|---|---|
+| JDK | 25.0.5+8 (Temurin, LTS) | `java -version` |
+| JUnit | 6.1.3 | `maven-metadata.xml` on Maven Central (`latest` = `release`) |
+| Maven | 3.9.11 | `mvn -version` |
+
+```bash
+export JAVA_HOME=/path/to/jdk-25 && export PATH="$JAVA_HOME/bin:$PATH"
+cd dynamic-programming
+mvn test # 11 tests; rewrites output/01-11
+./scripts/run-all.sh # also regenerates output/12 (SpaceDemo under a 48 MB heap)
+./scripts/make-single-file.sh # builds DynamicProgrammingTenProblems.java: java DynamicProgrammingTenProblems.java
+```
+
+| # | Problem | Source |
+|---|---|---|
+| 1 | Fibonacci | `Fibonacci` |
+| 2 | Climbing Stairs | `ClimbingStairs` |
+| 3 | House Robber | `HouseRobber` |
+| 4 | Coin Change (fewest coins, and number of ways) | `CoinChange` |
+| 5 | Longest Increasing Subsequence | `LongestIncreasingSubsequence` |
+| 6 | 0/1 Knapsack | `Knapsack01` |
+| 7 | Longest Common Subsequence | `Lcs` |
+| 8 | Edit Distance | `EditDistance` |
+| 9 | Unique Paths | `UniquePaths` |
+| 10 | Partition Equal Subset Sum | `PartitionEqualSubsetSum` |
+
+Captured output is in `output/`: `01` Fibonacci call counts, `02` Fibonacci pitfalls (`computeIfAbsent`, `long` overflow,
+stack depth), `03`-`10` one dry run per problem, `11` the cross-check summary, `12` the heap-limited space demo.
diff --git a/dynamic-programming/output/01-fibonacci-call-counts.txt b/dynamic-programming/output/01-fibonacci-call-counts.txt
new file mode 100644
index 0000000..52060a6
--- /dev/null
+++ b/dynamic-programming/output/01-fibonacci-call-counts.txt
@@ -0,0 +1,12 @@
+Fibonacci: how many calls does each approach make?
+==================================================
+
+n fib(n) naive calls memo calls table steps
+10 55 177 19 9
+20 6765 21891 39 19
+30 832040 2692537 59 29
+40 102334155 331160281 79 39
+
+naive calls = every call the recursion made (counted, not predicted).
+memo calls = the same recursion with a notebook: 2n-1 calls, because each F(k) is computed once.
+table steps = additions in the bottom-up loop (n-1).
diff --git a/dynamic-programming/output/02-fibonacci-pitfalls.txt b/dynamic-programming/output/02-fibonacci-pitfalls.txt
new file mode 100644
index 0000000..e1e164c
--- /dev/null
+++ b/dynamic-programming/output/02-fibonacci-pitfalls.txt
@@ -0,0 +1,16 @@
+Fibonacci: three things that go wrong
+=====================================
+
+1) HashMap.computeIfAbsent as the memo, recursive mapping function, n = 10
+ -> java.util.ConcurrentModificationException
+ same idea written as get / compute / put -> 55
+
+2) The answer outgrows a long
+ fib(92) = 7540113804746346429
+ fib(93) -> ArithmeticException: long overflow (Math.addExact refuses to wrap around)
+ plain + instead of addExact would silently return -6246583658587674878
+
+3) Recursion depth, House Robber with every house worth 1, in a thread with a 256 KB stack
+ memo n = 500 -> 250
+ memo n = 50,000 -> java.lang.StackOverflowError
+ tabulation n = 50,000 -> 25000
diff --git a/dynamic-programming/output/03-house-robber-table.txt b/dynamic-programming/output/03-house-robber-table.txt
new file mode 100644
index 0000000..3de956f
--- /dev/null
+++ b/dynamic-programming/output/03-house-robber-table.txt
@@ -0,0 +1,14 @@
+House Robber: money = [2, 7, 9, 3, 1]
+=====================================
+
+dp[k] = best total using only the first k houses
+k [0, 1, 2, 3, 4, 5]
+dp[k] [0, 2, 7, 11, 11, 12]
+
+k=1 house worth 2: skip = dp[0] = 0, take = dp[0] + 2 = 2 -> dp[1] = 2
+k=2 house worth 7: skip = dp[1] = 2, take = dp[0] + 7 = 7 -> dp[2] = 7
+k=3 house worth 9: skip = dp[2] = 7, take = dp[1] + 9 = 11 -> dp[3] = 11
+k=4 house worth 3: skip = dp[3] = 11, take = dp[2] + 3 = 10 -> dp[4] = 11
+k=5 house worth 1: skip = dp[4] = 11, take = dp[3] + 1 = 12 -> dp[5] = 12
+
+answer = 12 (houses worth 2, 9 and 1)
diff --git a/dynamic-programming/output/04-coin-change.txt b/dynamic-programming/output/04-coin-change.txt
new file mode 100644
index 0000000..a4ab6dd
--- /dev/null
+++ b/dynamic-programming/output/04-coin-change.txt
@@ -0,0 +1,18 @@
+Coin Change: greedy, the overflow bug, call counts, loop order
+==============================================================
+
+1) Greedy fails: coins {1,3,4}, amount 6
+ greedy -> 3 coins (4+1+1)
+ DP (table) -> 2 coins (3+3)
+
+2) 'Impossible' = Integer.MAX_VALUE, then dp[a-c] + 1: coins {2}, amount 3 (really impossible)
+ correct (impossible = amount + 1) -> -1
+ Integer.MAX_VALUE version -> -2147483648
+
+3) Calls made: coins {1,2,5}, amount 27, answer 6
+ naive recursion -> 2688332 calls
+ memoized -> 77 calls
+
+4) Loop order changes the question: coins {1,2,5}, amount 5
+ coins in the OUTER loop -> 4 (combinations: 5 / 2+2+1 / 2+1+1+1 / 1x5)
+ amount in the OUTER loop -> 9 (permutations: 1+2 and 2+1 now count separately)
diff --git a/dynamic-programming/output/05-knapsack.txt b/dynamic-programming/output/05-knapsack.txt
new file mode 100644
index 0000000..196afc4
--- /dev/null
+++ b/dynamic-programming/output/05-knapsack.txt
@@ -0,0 +1,19 @@
+0/1 Knapsack
+============
+
+1) Table: weights [1,3,4,5], values [1,4,5,7], capacity 7. dp[i][w] = best value with the first i items
+w = 0 1 2 3 4 5 6 7
+none 0 0 0 0 0 0 0 0
+item 1 0 1 1 1 1 1 1 1
+item 2 0 1 1 4 5 5 5 5
+item 3 0 1 1 4 5 6 6 9
+item 4 0 1 1 4 5 7 8 9
+answer = dp[4][7] = 9 (weights 3 + 4, values 4 + 5)
+
+2) One row, capacity loop direction: weights [5,4,6,3], values [10,40,30,50], capacity 10
+ backward loop (each item used once) -> 90
+ forward loop, same code otherwise -> 150 (the weight-3 item is reused three times: 3 x 50)
+
+3) Calls made: 22 items, capacity 60, best value 357
+ naive recursion -> 1003985 calls
+ memoized -> 1794 calls
diff --git a/dynamic-programming/output/06-lcs.txt b/dynamic-programming/output/06-lcs.txt
new file mode 100644
index 0000000..7475a43
--- /dev/null
+++ b/dynamic-programming/output/06-lcs.txt
@@ -0,0 +1,20 @@
+Longest Common Subsequence: "ABCBDAB" and "BDCABA"
+==================================================
+
+ - B D C A B A
+- 0 0 0 0 0 0 0
+A 0 0 0 0 1 1 1
+B 0 1 1 1 1 2 2
+C 0 1 1 2 2 2 2
+B 0 1 1 2 2 3 3
+D 0 1 2 2 2 3 3
+A 0 1 2 2 3 3 4
+B 0 1 2 2 3 4 4
+
+length (bottom-right cell) = 4
+reconstructed from the full table = "BCBA" (a valid subsequence of both strings)
+two-row version = 4 (length only; cannot rebuild the string)
+
+Calls made on two 15-character strings "ACGTTGCAACGTGCA" and "TGCATGCCGATGACG" (answer 9):
+ naive recursion -> 293086 calls
+ memoized -> 247 calls
diff --git a/dynamic-programming/output/07-edit-distance.txt b/dynamic-programming/output/07-edit-distance.txt
new file mode 100644
index 0000000..4ea79e0
--- /dev/null
+++ b/dynamic-programming/output/07-edit-distance.txt
@@ -0,0 +1,16 @@
+Edit Distance: "kitten" -> "sitting"
+====================================
+
+ - s i t t i n g
+- 0 1 2 3 4 5 6 7
+k 1 1 2 3 4 5 6 7
+i 2 2 1 2 3 4 5 6
+t 3 3 2 1 2 3 4 5
+t 4 4 3 2 1 2 3 4
+e 5 5 4 3 2 2 3 4
+n 6 6 5 4 3 3 2 3
+
+answer = 3 (k->s, e->i, insert g)
+two-row version = 3
+horse -> ros = 3
+intention -> execution = 5
diff --git a/dynamic-programming/output/08-lis.txt b/dynamic-programming/output/08-lis.txt
new file mode 100644
index 0000000..021408c
--- /dev/null
+++ b/dynamic-programming/output/08-lis.txt
@@ -0,0 +1,8 @@
+Longest Increasing Subsequence: [10, 9, 2, 5, 3, 7, 101, 18]
+============================================================
+
+a[i] [10, 9, 2, 5, 3, 7, 101, 18]
+dp[i] [1, 1, 1, 2, 2, 3, 4, 4] (dp[i] = longest increasing subsequence ENDING at index i)
+
+answer = max(dp) = 4 (for example 2, 3, 7, 18)
+O(n log n) patience version = 4
diff --git a/dynamic-programming/output/09-unique-paths.txt b/dynamic-programming/output/09-unique-paths.txt
new file mode 100644
index 0000000..8befbbe
--- /dev/null
+++ b/dynamic-programming/output/09-unique-paths.txt
@@ -0,0 +1,10 @@
+Unique Paths on a 3 x 7 grid (right and down only)
+==================================================
+
+ 0 1 2 3 4 5 6
+row 0 1 1 1 1 1 1 1
+row 1 1 2 3 4 5 6 7
+row 2 1 3 6 10 15 21 28
+
+answer = bottom-right cell = 28
+10 x 10 grid: table = 48620, one row = 48620, binomial C(18,9) = 48620
diff --git a/dynamic-programming/output/10-partition.txt b/dynamic-programming/output/10-partition.txt
new file mode 100644
index 0000000..0dc3ae6
--- /dev/null
+++ b/dynamic-programming/output/10-partition.txt
@@ -0,0 +1,14 @@
+Partition Equal Subset Sum: [1, 5, 11, 5], target 11
+====================================================
+
+T = some subset of the first i numbers sums to s
+s = 0 1 2 3 4 5 6 7 8 9 10 11
+none T . . . . . . . . . . .
++1 T T . . . . . . . . . .
++5 T T . . . T T . . . . .
++11 T T . . . T T . . . . T
++5 T T . . . T T . . . T T
+
+[1, 5, 11, 5] -> true (5 + 5 + 1 = 11 = 11)
+[1, 2, 3, 5] -> false (total 11 is odd)
+[1, 2, 4] -> false (total 7 is odd)
diff --git a/dynamic-programming/output/11-all-variants-agree.txt b/dynamic-programming/output/11-all-variants-agree.txt
new file mode 100644
index 0000000..24d5ed5
--- /dev/null
+++ b/dynamic-programming/output/11-all-variants-agree.txt
@@ -0,0 +1,15 @@
+Cross-check: 300 random inputs per problem, every variant must agree (seed 2026)
+================================================================================
+
+problem cases variants compared
+Climbing Stairs 300 memo = table = two variables (n up to 39)
+House Robber 300 memo = table = two variables
+Coin Change (fewest) 300 naive = memo = table (amount up to 29)
+Longest Increasing Subseq. 300 memo = O(n^2) table = O(n log n) patience
+0/1 Knapsack 300 naive = memo = 2-D table = one row (backward)
+Longest Common Subsequence 300 naive = memo = full table = two rows
+Edit Distance 300 memo = full table = two rows
+Unique Paths 300 memo = table = one row = binomial formula
+Partition Equal Subset Sum 300 memo = 2-D table = one row (backward)
+
+Fibonacci is cross-checked in 01-fibonacci-call-counts.txt (naive = memo = table = two variables).
diff --git a/dynamic-programming/output/12-space-demo.txt b/dynamic-programming/output/12-space-demo.txt
new file mode 100644
index 0000000..99b1975
--- /dev/null
+++ b/dynamic-programming/output/12-space-demo.txt
@@ -0,0 +1,6 @@
+max heap (MiB) : 48
+strings : two of length 5000
+full table cells : 25010001 ints = 100 MB
+two-row cells : 10002 ints = 40 KB
+two rows LCS length : 2500
+full table LCS length : java.lang.OutOfMemoryError: Java heap space
diff --git a/dynamic-programming/pom.xml b/dynamic-programming/pom.xml
new file mode 100644
index 0000000..f423d89
--- /dev/null
+++ b/dynamic-programming/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+ com.ankurm.interviewlab
+ dynamic-programming
+ 1.0.0
+ jar
+
+ java-interview-lab :: dynamic-programming
+
+ Runnable companion code for ankurm.com's "Dynamic Programming in Java for Beginners" article:
+ ten classic problems, each solved by memoization, tabulation and a space-optimised table,
+ with tests that cross-check every variant and write the transcripts the article quotes.
+
+
+
+ 25
+ UTF-8
+ 6.1.3
+
+
+
+
+
+ org.junit
+ junit-bom
+ ${junit.version}
+ pom
+ import
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.junit.platform
+ junit-platform-launcher
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.6.0
+
+ -Xmx512m -Xss1m -Duser.timezone=UTC -Dfile.encoding=UTF-8
+
+
+
+
+
diff --git a/dynamic-programming/scripts/make-single-file.sh b/dynamic-programming/scripts/make-single-file.sh
new file mode 100755
index 0000000..14dcd3f
--- /dev/null
+++ b/dynamic-programming/scripts/make-single-file.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# Builds one self-contained DynamicProgrammingTenProblems.java from the ten problem classes, so it runs with
+# java DynamicProgrammingTenProblems.java
+# No Maven needed. Output goes to ./DynamicProgrammingTenProblems.java (not committed).
+set -euo pipefail
+cd "$(dirname "$0")/.."
+SRC=src/main/java/com/ankurm/interviewlab/dp
+OUT=${1:-DynamicProgrammingTenProblems.java}
+python3 - "$SRC" "$OUT" <<'PY'
+import re, sys, pathlib
+src, out = pathlib.Path(sys.argv[1]), sys.argv[2]
+order = ["Fibonacci","ClimbingStairs","HouseRobber","CoinChange","LongestIncreasingSubsequence",
+ "Knapsack01","Lcs","EditDistance","UniquePaths","PartitionEqualSubsetSum"]
+imports, bodies = set(), []
+for name in order:
+ text = (src / f"{name}.java").read_text()
+ for m in re.finditer(r"^import [^;]+;", text, re.M): imports.add(m.group(0))
+ text = re.sub(r"^package [^;]+;\s*", "", text, flags=re.M)
+ text = re.sub(r"^import [^;]+;\s*", "", text, flags=re.M)
+ text = text.replace("public final class", "final class", 1)
+ bodies.append(text.strip())
+main = '''
+/** Prints a one-line answer for each of the ten problems. Run: java DynamicProgrammingTenProblems.java */
+public class DynamicProgrammingTenProblems {
+ public static void main(String[] args) {
+ long[] calls = {0};
+ System.out.println("1 Fibonacci(40) naive = " + Fibonacci.naive(40, calls) + " (" + calls[0] + " calls)");
+ calls[0] = 0;
+ System.out.println(" memo = " + Fibonacci.memo(40, calls) + " (" + calls[0] + " calls)");
+ System.out.println("2 Climbing stairs(10) " + ClimbingStairs.tabulation(10));
+ System.out.println("3 House robber [2,7,9,3,1] " + HouseRobber.tabulation(new int[] {2, 7, 9, 3, 1}));
+ System.out.println("4 Coin change {1,3,4}, 6 " + CoinChange.tabulation(new int[] {1, 3, 4}, 6) + " coins (greedy says " + CoinChange.greedy(new int[] {1, 3, 4}, 6) + ")");
+ System.out.println("5 LIS [10,9,2,5,3,7,101,18] " + LongestIncreasingSubsequence.tabulation(new int[] {10, 9, 2, 5, 3, 7, 101, 18}));
+ System.out.println("6 Knapsack 0/1 " + Knapsack01.oneRowBackward(new int[] {5, 4, 6, 3}, new int[] {10, 40, 30, 50}, 10));
+ System.out.println("7 LCS ABCBDAB / BDCABA " + Lcs.tabulation("ABCBDAB", "BDCABA"));
+ System.out.println("8 Edit distance kitten/sitting " + EditDistance.tabulation("kitten", "sitting"));
+ System.out.println("9 Unique paths 3 x 7 " + UniquePaths.tabulation(3, 7));
+ System.out.println("10 Partition [1,5,11,5] " + PartitionEqualSubsetSum.tabulation(new int[] {1, 5, 11, 5}));
+ }
+}
+'''
+header = "// Generated by scripts/make-single-file.sh from the java-interview-lab/dynamic-programming module.\n// Run with: java DynamicProgrammingTenProblems.java (JDK 25)\n"
+pathlib.Path(out).write_text(header + "\n".join(sorted(imports)) + "\n" + main + "\n" + "\n\n".join(bodies) + "\n")
+PY
diff --git a/dynamic-programming/scripts/run-all.sh b/dynamic-programming/scripts/run-all.sh
new file mode 100755
index 0000000..738292b
--- /dev/null
+++ b/dynamic-programming/scripts/run-all.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# Regenerates every file in output/. Needs JDK 25 and Maven. The first Maven run must be online.
+# 01-11 are written by JUnit tests that assert the same numbers they print (deterministic).
+# 12 runs SpaceDemo under a 48 MB heap: the outcome is deterministic, the "max heap" line is whatever the JVM reports.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+mkdir -p output
+mvn -B -q -T1 test
+mvn -B -q -T1 compile
+{ java -Xmx48m -cp target/classes com.ankurm.interviewlab.dp.SpaceDemo 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' ; } > output/12-space-demo.txt
+echo "done:"; ls -1 output
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/ClimbingStairs.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/ClimbingStairs.java
new file mode 100644
index 0000000..0a6c048
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/ClimbingStairs.java
@@ -0,0 +1,37 @@
+package com.ankurm.interviewlab.dp;
+
+/**
+ * Problem 2: Climbing Stairs. You may climb 1 or 2 steps at a time. How many distinct ways reach step n?
+ * ways(n) = ways(n-1) + ways(n-2): the last move was either a 1-step or a 2-step. (It is Fibonacci in disguise.)
+ */
+public final class ClimbingStairs {
+ private ClimbingStairs() {}
+
+ public static long memo(int n) {
+ return memo(n, new long[n + 2]);
+ }
+
+ private static long memo(int n, long[] notebook) {
+ if (n <= 1) return 1; // one way to stand still, one way to take a single step
+ if (notebook[n] != 0) return notebook[n];
+ return notebook[n] = memo(n - 1, notebook) + memo(n - 2, notebook);
+ }
+
+ public static long tabulation(int n) {
+ long[] dp = new long[n + 2];
+ dp[0] = 1;
+ dp[1] = 1;
+ for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
+ return dp[n];
+ }
+
+ public static long twoVariables(int n) {
+ long prev = 1, curr = 1;
+ for (int i = 2; i <= n; i++) {
+ long next = prev + curr;
+ prev = curr;
+ curr = next;
+ }
+ return curr;
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/CoinChange.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/CoinChange.java
new file mode 100644
index 0000000..d350fa4
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/CoinChange.java
@@ -0,0 +1,106 @@
+package com.ankurm.interviewlab.dp;
+
+import java.util.Arrays;
+
+/**
+ * Problem 4: Coin Change. Fewest coins that add up to an amount (unlimited supply of each coin), or -1 if impossible.
+ * fewest(a) = 1 + min over coins c of fewest(a - c).
+ * Also: how many ways can the coins make the amount, and why the loop order of that table matters.
+ */
+public final class CoinChange {
+ private CoinChange() {}
+
+ /** Greedy "always take the biggest coin". Wrong for some coin systems, e.g. {1,3,4} and amount 6. */
+ public static int greedy(int[] coins, int amount) {
+ int[] sorted = coins.clone();
+ Arrays.sort(sorted);
+ int count = 0;
+ for (int i = sorted.length - 1; i >= 0 && amount > 0; i--) {
+ count += amount / sorted[i];
+ amount %= sorted[i];
+ }
+ return amount == 0 ? count : -1;
+ }
+
+ /** Plain recursion; {@code calls[0]} counts the calls. Exponential. */
+ public static int naive(int[] coins, int amount, long[] calls) {
+ calls[0]++;
+ if (amount == 0) return 0;
+ int best = -1;
+ for (int c : coins) {
+ if (c > amount) continue;
+ int sub = naive(coins, amount - c, calls);
+ if (sub >= 0 && (best == -1 || sub + 1 < best)) best = sub + 1;
+ }
+ return best;
+ }
+
+ /** Top-down with a notebook. -2 = "not solved", -1 = "solved: impossible". */
+ public static int memo(int[] coins, int amount, long[] calls) {
+ int[] notebook = new int[amount + 1];
+ Arrays.fill(notebook, -2);
+ return memo(coins, amount, notebook, calls);
+ }
+
+ private static int memo(int[] coins, int amount, int[] notebook, long[] calls) {
+ calls[0]++;
+ if (amount == 0) return 0;
+ if (notebook[amount] != -2) return notebook[amount];
+ int best = -1;
+ for (int c : coins) {
+ if (c > amount) continue;
+ int sub = memo(coins, amount - c, notebook, calls);
+ if (sub >= 0 && (best == -1 || sub + 1 < best)) best = sub + 1;
+ }
+ return notebook[amount] = best;
+ }
+
+ /** Bottom-up. "Impossible" is amount + 1, a value that can never be a real answer, so adding 1 cannot overflow. */
+ public static int tabulation(int[] coins, int amount) {
+ int impossible = amount + 1;
+ int[] dp = new int[amount + 1];
+ Arrays.fill(dp, impossible);
+ dp[0] = 0;
+ for (int a = 1; a <= amount; a++) {
+ for (int c : coins) {
+ if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
+ }
+ }
+ return dp[amount] >= impossible ? -1 : dp[amount];
+ }
+
+ /** The classic bug: Integer.MAX_VALUE as "impossible", then dp[a - c] + 1 wraps around to a negative number. */
+ public static int tabulationOverflowBug(int[] coins, int amount) {
+ int[] dp = new int[amount + 1];
+ Arrays.fill(dp, Integer.MAX_VALUE);
+ dp[0] = 0;
+ for (int a = 1; a <= amount; a++) {
+ for (int c : coins) {
+ if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
+ }
+ }
+ return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
+ }
+
+ /** Number of COMBINATIONS (order ignored): coins in the OUTER loop. 1D array, which is already the space-optimised form. */
+ public static long countCombinations(int[] coins, int amount) {
+ long[] ways = new long[amount + 1];
+ ways[0] = 1;
+ for (int c : coins) {
+ for (int a = c; a <= amount; a++) ways[a] += ways[a - c];
+ }
+ return ways[amount];
+ }
+
+ /** Swap the loops (amount outer) and the same table now counts PERMUTATIONS (order matters): a different problem. */
+ public static long countPermutations(int[] coins, int amount) {
+ long[] ways = new long[amount + 1];
+ ways[0] = 1;
+ for (int a = 1; a <= amount; a++) {
+ for (int c : coins) {
+ if (c <= a) ways[a] += ways[a - c];
+ }
+ }
+ return ways[amount];
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/EditDistance.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/EditDistance.java
new file mode 100644
index 0000000..6c0e161
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/EditDistance.java
@@ -0,0 +1,65 @@
+package com.ankurm.interviewlab.dp;
+
+import java.util.Arrays;
+
+/**
+ * Problem 8: Edit Distance (Levenshtein). Fewest insert / delete / replace operations that turn a into b.
+ * dist(i, j) = a[i-1]==b[j-1] ? dist(i-1, j-1) : 1 + min(dist(i-1, j) [delete], dist(i, j-1) [insert], dist(i-1, j-1) [replace]).
+ */
+public final class EditDistance {
+ private EditDistance() {}
+
+ public static int memo(String a, String b) {
+ int[][] notebook = new int[a.length() + 1][b.length() + 1];
+ for (int[] row : notebook) Arrays.fill(row, -1);
+ return memo(a, b, a.length(), b.length(), notebook);
+ }
+
+ private static int memo(String a, String b, int i, int j, int[][] notebook) {
+ if (i == 0) return j; // only inserts left
+ if (j == 0) return i; // only deletes left
+ if (notebook[i][j] != -1) return notebook[i][j];
+ int result;
+ if (a.charAt(i - 1) == b.charAt(j - 1)) {
+ result = memo(a, b, i - 1, j - 1, notebook);
+ } else {
+ result = 1 + Math.min(memo(a, b, i - 1, j, notebook),
+ Math.min(memo(a, b, i, j - 1, notebook), memo(a, b, i - 1, j - 1, notebook)));
+ }
+ return notebook[i][j] = result;
+ }
+
+ public static int[][] table(String a, String b) {
+ int[][] dp = new int[a.length() + 1][b.length() + 1];
+ for (int i = 0; i <= a.length(); i++) dp[i][0] = i;
+ for (int j = 0; j <= b.length(); j++) dp[0][j] = j;
+ for (int i = 1; i <= a.length(); i++) {
+ for (int j = 1; j <= b.length(); j++) {
+ dp[i][j] = a.charAt(i - 1) == b.charAt(j - 1)
+ ? dp[i - 1][j - 1]
+ : 1 + Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1]));
+ }
+ }
+ return dp;
+ }
+
+ public static int tabulation(String a, String b) {
+ return table(a, b)[a.length()][b.length()];
+ }
+
+ public static int twoRows(String a, String b) {
+ int[] prev = new int[b.length() + 1];
+ int[] curr = new int[b.length() + 1];
+ for (int j = 0; j <= b.length(); j++) prev[j] = j;
+ for (int i = 1; i <= a.length(); i++) {
+ curr[0] = i;
+ for (int j = 1; j <= b.length(); j++) {
+ curr[j] = a.charAt(i - 1) == b.charAt(j - 1)
+ ? prev[j - 1]
+ : 1 + Math.min(prev[j], Math.min(curr[j - 1], prev[j - 1]));
+ }
+ int[] tmp = prev; prev = curr; curr = tmp;
+ }
+ return prev[b.length()];
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Fibonacci.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Fibonacci.java
new file mode 100644
index 0000000..b0929a7
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Fibonacci.java
@@ -0,0 +1,74 @@
+package com.ankurm.interviewlab.dp;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Problem 1: Fibonacci. F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2).
+ * The smallest problem that shows every idea in dynamic programming.
+ * All results are exact: Math.addExact throws once a value no longer fits in a long (n = 93).
+ */
+public final class Fibonacci {
+ private Fibonacci() {}
+
+ /** Plain recursion. Correct, and exponentially slow. {@code calls[0]} counts every call made. */
+ public static long naive(int n, long[] calls) {
+ calls[0]++;
+ if (n < 2) return n;
+ return Math.addExact(naive(n - 1, calls), naive(n - 2, calls));
+ }
+
+ /** Top-down: the same recursion, plus a notebook (memo) so each n is solved only once. */
+ public static long memo(int n, long[] calls) {
+ return memo(n, new long[n + 1], calls);
+ }
+
+ private static long memo(int n, long[] notebook, long[] calls) {
+ calls[0]++;
+ if (n < 2) return n;
+ if (notebook[n] != 0) return notebook[n]; // 0 means "not solved yet" (F(n) > 0 for n >= 1)
+ return notebook[n] = Math.addExact(memo(n - 1, notebook, calls), memo(n - 2, notebook, calls));
+ }
+
+ /** Bottom-up: fill a table from the smallest subproblem upwards. No recursion at all. */
+ public static long tabulation(int n) {
+ if (n < 2) return n;
+ long[] dp = new long[n + 1];
+ dp[1] = 1;
+ for (int i = 2; i <= n; i++) dp[i] = Math.addExact(dp[i - 1], dp[i - 2]);
+ return dp[n];
+ }
+
+ /** Space-optimised: dp[i] only needs the previous two cells, so keep two variables. */
+ public static long twoVariables(int n) {
+ if (n < 2) return n;
+ long prev = 0, curr = 1;
+ for (int i = 2; i <= n; i++) {
+ long next = Math.addExact(prev, curr);
+ prev = curr;
+ curr = next;
+ }
+ return curr;
+ }
+
+ /**
+ * The tempting one-liner memo. On JDK 9 and later HashMap.computeIfAbsent detects that the mapping
+ * function changed the map and throws ConcurrentModificationException.
+ */
+ public static long memoWithComputeIfAbsent(int n, Map cache) {
+ if (n < 2) return n;
+ return cache.computeIfAbsent(n, k -> memoWithComputeIfAbsent(k - 1, cache) + memoWithComputeIfAbsent(k - 2, cache));
+ }
+
+ /** The safe version of the same idea: look up, compute, then put. */
+ public static long memoWithGetAndPut(int n, Map cache) {
+ if (n < 2) return n;
+ Long hit = cache.get(n);
+ if (hit != null) return hit;
+ long value = memoWithGetAndPut(n - 1, cache) + memoWithGetAndPut(n - 2, cache);
+ cache.put(n, value);
+ return value;
+ }
+
+ public static Map newCache() { return new HashMap<>(); }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/HouseRobber.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/HouseRobber.java
new file mode 100644
index 0000000..2a63986
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/HouseRobber.java
@@ -0,0 +1,46 @@
+package com.ankurm.interviewlab.dp;
+
+/**
+ * Problem 3: House Robber. Pick houses to maximise money, never two neighbours.
+ * best(i) = max(best(i-1), best(i-2) + money[i]): skip house i, or take it and jump over its neighbour.
+ */
+public final class HouseRobber {
+ private HouseRobber() {}
+
+ public static int memo(int[] money) {
+ int[] notebook = new int[money.length];
+ java.util.Arrays.fill(notebook, -1); // -1 = "not solved"; 0 is a legitimate answer
+ return memo(money, money.length - 1, notebook);
+ }
+
+ private static int memo(int[] money, int i, int[] notebook) {
+ if (i < 0) return 0;
+ if (notebook[i] != -1) return notebook[i];
+ return notebook[i] = Math.max(memo(money, i - 1, notebook), memo(money, i - 2, notebook) + money[i]);
+ }
+
+ /** dp[k] = best total using only the first k houses. dp[0] = 0 (no houses). */
+ public static int[] table(int[] money) {
+ int[] dp = new int[money.length + 1];
+ for (int k = 1; k <= money.length; k++) {
+ int skip = dp[k - 1];
+ int take = (k >= 2 ? dp[k - 2] : 0) + money[k - 1];
+ dp[k] = Math.max(skip, take);
+ }
+ return dp;
+ }
+
+ public static int tabulation(int[] money) {
+ return table(money)[money.length];
+ }
+
+ public static int twoVariables(int[] money) {
+ int prev2 = 0, prev1 = 0; // best up to two houses ago, best up to the previous house
+ for (int m : money) {
+ int best = Math.max(prev1, prev2 + m);
+ prev2 = prev1;
+ prev1 = best;
+ }
+ return prev1;
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Knapsack01.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Knapsack01.java
new file mode 100644
index 0000000..ce0e251
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Knapsack01.java
@@ -0,0 +1,81 @@
+package com.ankurm.interviewlab.dp;
+
+import java.util.Arrays;
+
+/**
+ * Problem 6: 0/1 Knapsack. Each item may be taken at most once. Maximise total value within a weight capacity.
+ * best(i, w) = max(best(i-1, w), value[i] + best(i-1, w - weight[i])): leave item i, or take it.
+ */
+public final class Knapsack01 {
+ private Knapsack01() {}
+
+ public static int naive(int[] weight, int[] value, int capacity, long[] calls) {
+ return naive(weight, value, weight.length - 1, capacity, calls);
+ }
+
+ private static int naive(int[] weight, int[] value, int i, int cap, long[] calls) {
+ calls[0]++;
+ if (i < 0) return 0;
+ int leave = naive(weight, value, i - 1, cap, calls);
+ if (weight[i] > cap) return leave;
+ return Math.max(leave, value[i] + naive(weight, value, i - 1, cap - weight[i], calls));
+ }
+
+ /** Top-down: the state is TWO numbers (item index, remaining capacity), so the notebook is 2-D. */
+ public static int memo(int[] weight, int[] value, int capacity, long[] calls) {
+ int[][] notebook = new int[weight.length][capacity + 1];
+ for (int[] row : notebook) Arrays.fill(row, -1);
+ return memo(weight, value, weight.length - 1, capacity, notebook, calls);
+ }
+
+ private static int memo(int[] weight, int[] value, int i, int cap, int[][] notebook, long[] calls) {
+ calls[0]++;
+ if (i < 0) return 0;
+ if (notebook[i][cap] != -1) return notebook[i][cap];
+ int leave = memo(weight, value, i - 1, cap, notebook, calls);
+ int best = leave;
+ if (weight[i] <= cap) best = Math.max(leave, value[i] + memo(weight, value, i - 1, cap - weight[i], notebook, calls));
+ return notebook[i][cap] = best;
+ }
+
+ /** dp[i][w] = best value using only the first i items with capacity w. */
+ public static int[][] table(int[] weight, int[] value, int capacity) {
+ int n = weight.length;
+ int[][] dp = new int[n + 1][capacity + 1];
+ for (int i = 1; i <= n; i++) {
+ for (int w = 0; w <= capacity; w++) {
+ dp[i][w] = dp[i - 1][w];
+ if (weight[i - 1] <= w) {
+ dp[i][w] = Math.max(dp[i][w], value[i - 1] + dp[i - 1][w - weight[i - 1]]);
+ }
+ }
+ }
+ return dp;
+ }
+
+ public static int tabulation(int[] weight, int[] value, int capacity) {
+ return table(weight, value, capacity)[weight.length][capacity];
+ }
+
+ /** One row is enough, but the capacity loop MUST run downwards so each item is used at most once. */
+ public static int oneRowBackward(int[] weight, int[] value, int capacity) {
+ int[] dp = new int[capacity + 1];
+ for (int i = 0; i < weight.length; i++) {
+ for (int w = capacity; w >= weight[i]; w--) {
+ dp[w] = Math.max(dp[w], value[i] + dp[w - weight[i]]);
+ }
+ }
+ return dp[capacity];
+ }
+
+ /** Same code with the loop flipped. It compiles, runs, and silently answers a different problem (unbounded knapsack). */
+ public static int oneRowForwardBug(int[] weight, int[] value, int capacity) {
+ int[] dp = new int[capacity + 1];
+ for (int i = 0; i < weight.length; i++) {
+ for (int w = weight[i]; w <= capacity; w++) {
+ dp[w] = Math.max(dp[w], value[i] + dp[w - weight[i]]);
+ }
+ }
+ return dp[capacity];
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Lcs.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Lcs.java
new file mode 100644
index 0000000..15d1815
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Lcs.java
@@ -0,0 +1,88 @@
+package com.ankurm.interviewlab.dp;
+
+import java.util.Arrays;
+
+/**
+ * Problem 7: Longest Common Subsequence of two strings (characters in order, not necessarily adjacent).
+ * lcs(i, j) = a[i-1]==b[j-1] ? 1 + lcs(i-1, j-1) : max(lcs(i-1, j), lcs(i, j-1)).
+ */
+public final class Lcs {
+ private Lcs() {}
+
+ public static int naive(String a, String b, long[] calls) {
+ return naive(a, b, a.length(), b.length(), calls);
+ }
+
+ private static int naive(String a, String b, int i, int j, long[] calls) {
+ calls[0]++;
+ if (i == 0 || j == 0) return 0;
+ if (a.charAt(i - 1) == b.charAt(j - 1)) return 1 + naive(a, b, i - 1, j - 1, calls);
+ return Math.max(naive(a, b, i - 1, j, calls), naive(a, b, i, j - 1, calls));
+ }
+
+ public static int memo(String a, String b, long[] calls) {
+ int[][] notebook = new int[a.length() + 1][b.length() + 1];
+ for (int[] row : notebook) Arrays.fill(row, -1);
+ return memo(a, b, a.length(), b.length(), notebook, calls);
+ }
+
+ private static int memo(String a, String b, int i, int j, int[][] notebook, long[] calls) {
+ calls[0]++;
+ if (i == 0 || j == 0) return 0;
+ if (notebook[i][j] != -1) return notebook[i][j];
+ int result = a.charAt(i - 1) == b.charAt(j - 1)
+ ? 1 + memo(a, b, i - 1, j - 1, notebook, calls)
+ : Math.max(memo(a, b, i - 1, j, notebook, calls), memo(a, b, i, j - 1, notebook, calls));
+ return notebook[i][j] = result;
+ }
+
+ /** dp[i][j] = LCS length of the first i characters of a and the first j characters of b. */
+ public static int[][] table(String a, String b) {
+ int[][] dp = new int[a.length() + 1][b.length() + 1];
+ for (int i = 1; i <= a.length(); i++) {
+ for (int j = 1; j <= b.length(); j++) {
+ dp[i][j] = a.charAt(i - 1) == b.charAt(j - 1)
+ ? dp[i - 1][j - 1] + 1
+ : Math.max(dp[i - 1][j], dp[i][j - 1]);
+ }
+ }
+ return dp;
+ }
+
+ public static int tabulation(String a, String b) {
+ return table(a, b)[a.length()][b.length()];
+ }
+
+ /** Walk the full table backwards from the corner to recover an actual subsequence. Needs the whole table. */
+ public static String reconstruct(String a, String b, int[][] dp) {
+ StringBuilder sb = new StringBuilder();
+ int i = a.length(), j = b.length();
+ while (i > 0 && j > 0) {
+ if (a.charAt(i - 1) == b.charAt(j - 1)) {
+ sb.append(a.charAt(i - 1));
+ i--;
+ j--;
+ } else if (dp[i - 1][j] >= dp[i][j - 1]) {
+ i--;
+ } else {
+ j--;
+ }
+ }
+ return sb.reverse().toString();
+ }
+
+ /** Two rows instead of the full table: O(min-length) memory, but the answer is a length only; no reconstruction. */
+ public static int twoRows(String a, String b) {
+ int[] prev = new int[b.length() + 1];
+ int[] curr = new int[b.length() + 1];
+ for (int i = 1; i <= a.length(); i++) {
+ for (int j = 1; j <= b.length(); j++) {
+ curr[j] = a.charAt(i - 1) == b.charAt(j - 1)
+ ? prev[j - 1] + 1
+ : Math.max(prev[j], curr[j - 1]);
+ }
+ int[] tmp = prev; prev = curr; curr = tmp;
+ }
+ return prev[b.length()];
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/LongestIncreasingSubsequence.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/LongestIncreasingSubsequence.java
new file mode 100644
index 0000000..67a6c31
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/LongestIncreasingSubsequence.java
@@ -0,0 +1,59 @@
+package com.ankurm.interviewlab.dp;
+
+import java.util.Arrays;
+
+/**
+ * Problem 5: Longest Increasing Subsequence (strictly increasing, elements need not be adjacent).
+ * endingAt(i) = 1 + max endingAt(j) over j < i with a[j] < a[i]. The answer is the maximum over all i.
+ */
+public final class LongestIncreasingSubsequence {
+ private LongestIncreasingSubsequence() {}
+
+ public static int memo(int[] a) {
+ int[] notebook = new int[a.length]; // 0 = not solved (a real answer is at least 1)
+ int best = 0;
+ for (int i = 0; i < a.length; i++) best = Math.max(best, endingAt(a, i, notebook));
+ return best;
+ }
+
+ private static int endingAt(int[] a, int i, int[] notebook) {
+ if (notebook[i] != 0) return notebook[i];
+ int best = 1;
+ for (int j = 0; j < i; j++) {
+ if (a[j] < a[i]) best = Math.max(best, 1 + endingAt(a, j, notebook));
+ }
+ return notebook[i] = best;
+ }
+
+ /** O(n^2) table: dp[i] = length of the best increasing subsequence that ENDS at index i. */
+ public static int[] table(int[] a) {
+ int[] dp = new int[a.length];
+ Arrays.fill(dp, 1);
+ for (int i = 1; i < a.length; i++) {
+ for (int j = 0; j < i; j++) {
+ if (a[j] < a[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
+ }
+ }
+ return dp;
+ }
+
+ public static int tabulation(int[] a) {
+ return Arrays.stream(table(a)).max().orElse(0);
+ }
+
+ /**
+ * O(n log n) "patience" version: tails[k] = the smallest possible last value of an increasing subsequence of length k+1.
+ * Not a table of subproblems any more, but the interviewer's usual follow-up.
+ */
+ public static int patience(int[] a) {
+ int[] tails = new int[a.length];
+ int size = 0;
+ for (int x : a) {
+ int pos = Arrays.binarySearch(tails, 0, size, x);
+ if (pos < 0) pos = -(pos + 1); // first tail >= x
+ tails[pos] = x;
+ if (pos == size) size++;
+ }
+ return size;
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/PartitionEqualSubsetSum.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/PartitionEqualSubsetSum.java
new file mode 100644
index 0000000..b8d506b
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/PartitionEqualSubsetSum.java
@@ -0,0 +1,58 @@
+package com.ankurm.interviewlab.dp;
+
+import java.util.Arrays;
+
+/**
+ * Problem 10: Partition Equal Subset Sum. Can the numbers be split into two groups with the same sum?
+ * Equivalent question: can some subset reach exactly total/2? (A yes/no knapsack with no values.)
+ */
+public final class PartitionEqualSubsetSum {
+ private PartitionEqualSubsetSum() {}
+
+ public static boolean memo(int[] nums) {
+ int total = Arrays.stream(nums).sum();
+ if (total % 2 != 0) return false; // an odd total can never split evenly
+ int target = total / 2;
+ Boolean[][] notebook = new Boolean[nums.length][target + 1];
+ return memo(nums, nums.length - 1, target, notebook);
+ }
+
+ private static boolean memo(int[] nums, int i, int remaining, Boolean[][] notebook) {
+ if (remaining == 0) return true;
+ if (i < 0 || remaining < 0) return false;
+ if (notebook[i][remaining] != null) return notebook[i][remaining];
+ boolean result = memo(nums, i - 1, remaining - nums[i], notebook) || memo(nums, i - 1, remaining, notebook);
+ return notebook[i][remaining] = result;
+ }
+
+ /** dp[i][s] = can the first i numbers make sum s? */
+ public static boolean[][] table(int[] nums, int target) {
+ boolean[][] dp = new boolean[nums.length + 1][target + 1];
+ dp[0][0] = true;
+ for (int i = 1; i <= nums.length; i++) {
+ for (int s = 0; s <= target; s++) {
+ dp[i][s] = dp[i - 1][s] || (s >= nums[i - 1] && dp[i - 1][s - nums[i - 1]]);
+ }
+ }
+ return dp;
+ }
+
+ public static boolean tabulation(int[] nums) {
+ int total = Arrays.stream(nums).sum();
+ if (total % 2 != 0) return false;
+ return table(nums, total / 2)[nums.length][total / 2];
+ }
+
+ /** One boolean row; sums run downwards for the same reason as 0/1 knapsack. */
+ public static boolean oneRow(int[] nums) {
+ int total = Arrays.stream(nums).sum();
+ if (total % 2 != 0) return false;
+ int target = total / 2;
+ boolean[] dp = new boolean[target + 1];
+ dp[0] = true;
+ for (int x : nums) {
+ for (int s = target; s >= x; s--) dp[s] = dp[s] || dp[s - x];
+ }
+ return dp[target];
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/SpaceDemo.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/SpaceDemo.java
new file mode 100644
index 0000000..3569c81
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/SpaceDemo.java
@@ -0,0 +1,29 @@
+package com.ankurm.interviewlab.dp;
+
+/**
+ * Run with a deliberately small heap to see why the two-row table matters:
+ * {@code java -Xmx48m -cp target/classes com.ankurm.interviewlab.dp.SpaceDemo}
+ * Full LCS table for two 5,000-character strings = 5001 x 5001 ints = about 100 MB. Two rows = about 40 KB.
+ */
+public final class SpaceDemo {
+ public static void main(String[] args) {
+ int n = 5000;
+ String a = pattern(n, 7), b = pattern(n, 5);
+ System.out.println("max heap (MiB) : " + Runtime.getRuntime().maxMemory() / (1024 * 1024));
+ System.out.println("strings : two of length " + n);
+ System.out.println("full table cells : " + (long) (n + 1) * (n + 1) + " ints = " + (long) (n + 1) * (n + 1) * 4 / 1_000_000 + " MB");
+ System.out.println("two-row cells : " + 2L * (n + 1) + " ints = " + 2L * (n + 1) * 4 / 1000 + " KB");
+ System.out.println("two rows LCS length : " + Lcs.twoRows(a, b));
+ try {
+ System.out.println("full table LCS length : " + Lcs.tabulation(a, b));
+ } catch (OutOfMemoryError e) {
+ System.out.println("full table LCS length : " + e);
+ }
+ }
+
+ private static String pattern(int n, int step) {
+ StringBuilder sb = new StringBuilder(n);
+ for (int i = 0; i < n; i++) sb.append((char) ('a' + (i * step + i / 3) % 4));
+ return sb.toString();
+ }
+}
diff --git a/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/UniquePaths.java b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/UniquePaths.java
new file mode 100644
index 0000000..878572b
--- /dev/null
+++ b/dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/UniquePaths.java
@@ -0,0 +1,51 @@
+package com.ankurm.interviewlab.dp;
+
+/**
+ * Problem 9: Unique Paths. A robot on an m x n grid starts top-left and may only move right or down.
+ * How many routes reach the bottom-right? paths(r, c) = paths(r-1, c) + paths(r, c-1).
+ */
+public final class UniquePaths {
+ private UniquePaths() {}
+
+ public static long memo(int rows, int cols) {
+ return memo(rows - 1, cols - 1, new long[rows][cols]);
+ }
+
+ private static long memo(int r, int c, long[][] notebook) {
+ if (r == 0 || c == 0) return 1; // one straight line along an edge
+ if (notebook[r][c] != 0) return notebook[r][c];
+ return notebook[r][c] = memo(r - 1, c, notebook) + memo(r, c - 1, notebook);
+ }
+
+ public static long[][] table(int rows, int cols) {
+ long[][] dp = new long[rows][cols];
+ for (int r = 0; r < rows; r++) {
+ for (int c = 0; c < cols; c++) {
+ dp[r][c] = (r == 0 || c == 0) ? 1 : dp[r - 1][c] + dp[r][c - 1];
+ }
+ }
+ return dp;
+ }
+
+ public static long tabulation(int rows, int cols) {
+ return table(rows, cols)[rows - 1][cols - 1];
+ }
+
+ /** One row: row[c] already holds the cell above, row[c-1] is the cell to the left. */
+ public static long oneRow(int rows, int cols) {
+ long[] row = new long[cols];
+ java.util.Arrays.fill(row, 1);
+ for (int r = 1; r < rows; r++) {
+ for (int c = 1; c < cols; c++) row[c] += row[c - 1];
+ }
+ return row[cols - 1];
+ }
+
+ /** Closed form check: choose which (rows-1) of the (rows+cols-2) moves are "down". */
+ public static long binomial(int rows, int cols) {
+ long result = 1;
+ int k = Math.min(rows - 1, cols - 1), n = rows + cols - 2;
+ for (int i = 1; i <= k; i++) result = result * (n - k + i) / i; // stays an exact integer at every step
+ return result;
+ }
+}
diff --git a/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/FibonacciTest.java b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/FibonacciTest.java
new file mode 100644
index 0000000..8be80c6
--- /dev/null
+++ b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/FibonacciTest.java
@@ -0,0 +1,100 @@
+package com.ankurm.interviewlab.dp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.ankurm.interviewlab.dp.support.Transcript;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.Test;
+
+class FibonacciTest {
+
+ @Test
+ void callCounts() {
+ Transcript t = Transcript.start("01-fibonacci-call-counts.txt",
+ "Fibonacci: how many calls does each approach make?");
+ t.line(String.format("%-4s %-22s %-14s %-14s %s", "n", "fib(n)", "naive calls", "memo calls", "table steps"));
+ for (int n : new int[] {10, 20, 30, 40}) {
+ long[] naiveCalls = {0}, memoCalls = {0};
+ long a = Fibonacci.naive(n, naiveCalls);
+ long b = Fibonacci.memo(n, memoCalls);
+ assertEquals(a, b);
+ assertEquals(a, Fibonacci.tabulation(n));
+ assertEquals(a, Fibonacci.twoVariables(n));
+ // Known identity: the naive recursion makes 2*F(n+1)-1 calls. A real count, checked against it.
+ assertEquals(2 * Fibonacci.twoVariables(n + 1) - 1, naiveCalls[0]);
+ assertEquals(2L * n - 1, memoCalls[0]);
+ t.line(String.format("%-4d %-22d %-14d %-14d %d", n, a, naiveCalls[0], memoCalls[0], n - 1));
+ }
+ t.blank();
+ t.line("naive calls = every call the recursion made (counted, not predicted).");
+ t.line("memo calls = the same recursion with a notebook: 2n-1 calls, because each F(k) is computed once.");
+ t.line("table steps = additions in the bottom-up loop (n-1).");
+ t.flush();
+ }
+
+ @Test
+ void pitfalls() throws Exception {
+ Transcript t = Transcript.start("02-fibonacci-pitfalls.txt", "Fibonacci: three things that go wrong");
+
+ // 1. computeIfAbsent with a recursive mapping function.
+ Map cache = new HashMap<>();
+ ConcurrentModificationException cme = assertThrows(ConcurrentModificationException.class,
+ () -> Fibonacci.memoWithComputeIfAbsent(10, cache));
+ t.line("1) HashMap.computeIfAbsent as the memo, recursive mapping function, n = 10");
+ t.line(" -> " + cme.getClass().getName());
+ assertEquals(55L, Fibonacci.memoWithGetAndPut(10, new HashMap<>()));
+ t.line(" same idea written as get / compute / put -> " + Fibonacci.memoWithGetAndPut(10, new HashMap<>()));
+ t.blank();
+
+ // 2. long overflow.
+ long f92 = Fibonacci.tabulation(92);
+ ArithmeticException overflow = assertThrows(ArithmeticException.class, () -> Fibonacci.tabulation(93));
+ t.line("2) The answer outgrows a long");
+ t.line(" fib(92) = " + f92);
+ t.line(" fib(93) -> " + overflow.getClass().getSimpleName() + ": " + overflow.getMessage() + " (Math.addExact refuses to wrap around)");
+ long wrapped = 7540113804746346429L + 4660046610375530309L; // F(92) + F(91) with plain +
+ assertEquals(wrapped, f92 + Fibonacci.tabulation(91));
+ assertTrue(wrapped < 0);
+ t.line(" plain + instead of addExact would silently return " + wrapped);
+ t.blank();
+
+ // 3. recursion depth: memo recurses once per element, the table does not. Same JVM, a 256 KB thread stack.
+ int[] small = new int[500];
+ int[] big = new int[50_000];
+ java.util.Arrays.fill(small, 1);
+ java.util.Arrays.fill(big, 1);
+ AtomicReference