From cb1b28682f74fdce8b5d5517d69b336ea4bd48b3 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Mon, 9 Nov 2026 08:40:42 +0000 Subject: [PATCH] Add dynamic-programming module: ten classic DP problems, memoization vs tabulation vs space-optimised, call-count and pitfall transcripts (11 tests) --- README.md | 1 + dynamic-programming/README.md | 38 +++ .../output/01-fibonacci-call-counts.txt | 12 + .../output/02-fibonacci-pitfalls.txt | 16 ++ .../output/03-house-robber-table.txt | 14 ++ dynamic-programming/output/04-coin-change.txt | 18 ++ dynamic-programming/output/05-knapsack.txt | 19 ++ dynamic-programming/output/06-lcs.txt | 20 ++ .../output/07-edit-distance.txt | 16 ++ dynamic-programming/output/08-lis.txt | 8 + .../output/09-unique-paths.txt | 10 + dynamic-programming/output/10-partition.txt | 14 ++ .../output/11-all-variants-agree.txt | 15 ++ dynamic-programming/output/12-space-demo.txt | 6 + dynamic-programming/pom.xml | 65 +++++ .../scripts/make-single-file.sh | 44 ++++ dynamic-programming/scripts/run-all.sh | 11 + .../interviewlab/dp/ClimbingStairs.java | 37 +++ .../ankurm/interviewlab/dp/CoinChange.java | 106 ++++++++ .../ankurm/interviewlab/dp/EditDistance.java | 65 +++++ .../com/ankurm/interviewlab/dp/Fibonacci.java | 74 ++++++ .../ankurm/interviewlab/dp/HouseRobber.java | 46 ++++ .../ankurm/interviewlab/dp/Knapsack01.java | 81 ++++++ .../java/com/ankurm/interviewlab/dp/Lcs.java | 88 +++++++ .../dp/LongestIncreasingSubsequence.java | 59 +++++ .../dp/PartitionEqualSubsetSum.java | 58 +++++ .../com/ankurm/interviewlab/dp/SpaceDemo.java | 29 +++ .../ankurm/interviewlab/dp/UniquePaths.java | 51 ++++ .../ankurm/interviewlab/dp/FibonacciTest.java | 100 ++++++++ .../interviewlab/dp/ProblemsAgreeTest.java | 106 ++++++++ .../interviewlab/dp/TranscriptsTest.java | 234 ++++++++++++++++++ .../interviewlab/dp/support/Tables.java | 47 ++++ .../interviewlab/dp/support/Transcript.java | 43 ++++ 33 files changed, 1551 insertions(+) create mode 100644 dynamic-programming/README.md create mode 100644 dynamic-programming/output/01-fibonacci-call-counts.txt create mode 100644 dynamic-programming/output/02-fibonacci-pitfalls.txt create mode 100644 dynamic-programming/output/03-house-robber-table.txt create mode 100644 dynamic-programming/output/04-coin-change.txt create mode 100644 dynamic-programming/output/05-knapsack.txt create mode 100644 dynamic-programming/output/06-lcs.txt create mode 100644 dynamic-programming/output/07-edit-distance.txt create mode 100644 dynamic-programming/output/08-lis.txt create mode 100644 dynamic-programming/output/09-unique-paths.txt create mode 100644 dynamic-programming/output/10-partition.txt create mode 100644 dynamic-programming/output/11-all-variants-agree.txt create mode 100644 dynamic-programming/output/12-space-demo.txt create mode 100644 dynamic-programming/pom.xml create mode 100755 dynamic-programming/scripts/make-single-file.sh create mode 100755 dynamic-programming/scripts/run-all.sh create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/ClimbingStairs.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/CoinChange.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/EditDistance.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Fibonacci.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/HouseRobber.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Knapsack01.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/Lcs.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/LongestIncreasingSubsequence.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/PartitionEqualSubsetSum.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/SpaceDemo.java create mode 100644 dynamic-programming/src/main/java/com/ankurm/interviewlab/dp/UniquePaths.java create mode 100644 dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/FibonacciTest.java create mode 100644 dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/ProblemsAgreeTest.java create mode 100644 dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/TranscriptsTest.java create mode 100644 dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Tables.java create mode 100644 dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Transcript.java 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 smallMemo = new AtomicReference<>(), bigMemo = new AtomicReference<>(), bigTable = new AtomicReference<>(); + runWithStack(256 * 1024, () -> { + smallMemo.set(attempt(() -> HouseRobber.memo(small))); + bigMemo.set(attempt(() -> HouseRobber.memo(big))); + bigTable.set(attempt(() -> HouseRobber.tabulation(big))); + }); + t.line("3) Recursion depth, House Robber with every house worth 1, in a thread with a 256 KB stack"); + t.line(" memo n = 500 -> " + smallMemo.get()); + t.line(" memo n = 50,000 -> " + bigMemo.get()); + t.line(" tabulation n = 50,000 -> " + bigTable.get()); + assertEquals(250, smallMemo.get()); + assertEquals("java.lang.StackOverflowError", bigMemo.get()); + assertEquals(25_000, bigTable.get()); + t.flush(); + } + + private static Object attempt(java.util.function.IntSupplier s) { + try { + return s.getAsInt(); + } catch (StackOverflowError e) { + return e.getClass().getName(); + } + } + + private static void runWithStack(long bytes, Runnable r) throws InterruptedException { + Thread th = new Thread(null, r, "small-stack", bytes); + th.start(); + th.join(); + } +} diff --git a/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/ProblemsAgreeTest.java b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/ProblemsAgreeTest.java new file mode 100644 index 0000000..b721003 --- /dev/null +++ b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/ProblemsAgreeTest.java @@ -0,0 +1,106 @@ +package com.ankurm.interviewlab.dp; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.ankurm.interviewlab.dp.support.Transcript; +import java.util.Random; +import org.junit.jupiter.api.Test; + +/** Every variant of every problem must give the same answer on the same input. Fixed seed, so the run is repeatable. */ +class ProblemsAgreeTest { + private static final int CASES = 300; + + @Test + void allVariantsAgree() { + Random rnd = new Random(2026); + Transcript t = Transcript.start("11-all-variants-agree.txt", + "Cross-check: " + CASES + " random inputs per problem, every variant must agree (seed 2026)"); + t.line(String.format("%-28s %-8s %s", "problem", "cases", "variants compared")); + + for (int c = 0; c < CASES; c++) { + int n = rnd.nextInt(0, 40); + assertEquals(ClimbingStairs.tabulation(n), ClimbingStairs.memo(n)); + assertEquals(ClimbingStairs.tabulation(n), ClimbingStairs.twoVariables(n)); + } + t.line(String.format("%-28s %-8d %s", "Climbing Stairs", CASES, "memo = table = two variables (n up to 39)")); + + for (int c = 0; c < CASES; c++) { + int[] money = rnd.ints(rnd.nextInt(0, 30), 0, 50).toArray(); + int expected = HouseRobber.tabulation(money); + assertEquals(expected, HouseRobber.memo(money)); + assertEquals(expected, HouseRobber.twoVariables(money)); + } + t.line(String.format("%-28s %-8d %s", "House Robber", CASES, "memo = table = two variables")); + + for (int c = 0; c < CASES; c++) { + int[] coins = rnd.ints(rnd.nextInt(1, 4), 1, 9).distinct().toArray(); + int amount = rnd.nextInt(0, 30); + int expected = CoinChange.tabulation(coins, amount); + assertEquals(expected, CoinChange.memo(coins, amount, new long[1])); + assertEquals(expected, CoinChange.naive(coins, amount, new long[1])); + } + t.line(String.format("%-28s %-8d %s", "Coin Change (fewest)", CASES, "naive = memo = table (amount up to 29)")); + + for (int c = 0; c < CASES; c++) { + int[] a = rnd.ints(rnd.nextInt(0, 25), 0, 20).toArray(); + int expected = LongestIncreasingSubsequence.tabulation(a); + assertEquals(expected, LongestIncreasingSubsequence.memo(a)); + assertEquals(expected, LongestIncreasingSubsequence.patience(a)); + } + t.line(String.format("%-28s %-8d %s", "Longest Increasing Subseq.", CASES, "memo = O(n^2) table = O(n log n) patience")); + + for (int c = 0; c < CASES; c++) { + int n = rnd.nextInt(0, 12), cap = rnd.nextInt(0, 40); + int[] w = rnd.ints(n, 1, 15).toArray(), v = rnd.ints(n, 1, 30).toArray(); + int expected = Knapsack01.tabulation(w, v, cap); + assertEquals(expected, Knapsack01.naive(w, v, cap, new long[1])); + assertEquals(expected, Knapsack01.memo(w, v, cap, new long[1])); + assertEquals(expected, Knapsack01.oneRowBackward(w, v, cap)); + } + t.line(String.format("%-28s %-8d %s", "0/1 Knapsack", CASES, "naive = memo = 2-D table = one row (backward)")); + + for (int c = 0; c < CASES; c++) { + String a = word(rnd, rnd.nextInt(0, 12)), b = word(rnd, rnd.nextInt(0, 12)); + int expected = Lcs.tabulation(a, b); + assertEquals(expected, Lcs.naive(a, b, new long[1])); + assertEquals(expected, Lcs.memo(a, b, new long[1])); + assertEquals(expected, Lcs.twoRows(a, b)); + } + t.line(String.format("%-28s %-8d %s", "Longest Common Subsequence", CASES, "naive = memo = full table = two rows")); + + for (int c = 0; c < CASES; c++) { + String a = word(rnd, rnd.nextInt(0, 12)), b = word(rnd, rnd.nextInt(0, 12)); + int expected = EditDistance.tabulation(a, b); + assertEquals(expected, EditDistance.memo(a, b)); + assertEquals(expected, EditDistance.twoRows(a, b)); + } + t.line(String.format("%-28s %-8d %s", "Edit Distance", CASES, "memo = full table = two rows")); + + for (int c = 0; c < CASES; c++) { + int r = rnd.nextInt(1, 17), k = rnd.nextInt(1, 17); + long expected = UniquePaths.tabulation(r, k); + assertEquals(expected, UniquePaths.memo(r, k)); + assertEquals(expected, UniquePaths.oneRow(r, k)); + assertEquals(expected, UniquePaths.binomial(r, k)); + } + t.line(String.format("%-28s %-8d %s", "Unique Paths", CASES, "memo = table = one row = binomial formula")); + + for (int c = 0; c < CASES; c++) { + int[] nums = rnd.ints(rnd.nextInt(0, 12), 1, 20).toArray(); + boolean expected = PartitionEqualSubsetSum.tabulation(nums); + assertEquals(expected, PartitionEqualSubsetSum.memo(nums)); + assertEquals(expected, PartitionEqualSubsetSum.oneRow(nums)); + } + t.line(String.format("%-28s %-8d %s", "Partition Equal Subset Sum", CASES, "memo = 2-D table = one row (backward)")); + + t.blank(); + t.line("Fibonacci is cross-checked in 01-fibonacci-call-counts.txt (naive = memo = table = two variables)."); + t.flush(); + } + + private static String word(Random rnd, int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) sb.append((char) ('a' + rnd.nextInt(4))); + return sb.toString(); + } +} diff --git a/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/TranscriptsTest.java b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/TranscriptsTest.java new file mode 100644 index 0000000..0c2c19d --- /dev/null +++ b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/TranscriptsTest.java @@ -0,0 +1,234 @@ +package com.ankurm.interviewlab.dp; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.ankurm.interviewlab.dp.support.Tables; +import com.ankurm.interviewlab.dp.support.Transcript; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +/** One dry-run transcript per problem. Each test asserts the same numbers it prints. */ +class TranscriptsTest { + + @Test + void houseRobber() { + int[] money = {2, 7, 9, 3, 1}; + int[] dp = HouseRobber.table(money); + assertArrayEquals(new int[] {0, 2, 7, 11, 11, 12}, dp); + assertEquals(12, HouseRobber.memo(money)); + assertEquals(12, HouseRobber.twoVariables(money)); + + Transcript t = Transcript.start("03-house-robber-table.txt", "House Robber: money = [2, 7, 9, 3, 1]"); + t.line("dp[k] = best total using only the first k houses"); + t.line("k " + Arrays.toString(new int[] {0, 1, 2, 3, 4, 5})); + t.line("dp[k] " + Arrays.toString(dp)); + t.blank(); + for (int k = 1; k <= money.length; k++) { + int skip = dp[k - 1], take = (k >= 2 ? dp[k - 2] : 0) + money[k - 1]; + t.line(String.format("k=%d house worth %d: skip = dp[%d] = %d, take = dp[%d] + %d = %d -> dp[%d] = %d", + k, money[k - 1], k - 1, skip, Math.max(k - 2, 0), money[k - 1], take, k, dp[k])); + } + t.blank(); + t.line("answer = " + dp[money.length] + " (houses worth 2, 9 and 1)"); + t.flush(); + } + + @Test + void coinChange() { + int[] coins = {1, 3, 4}; + assertEquals(3, CoinChange.greedy(coins, 6)); + assertEquals(2, CoinChange.tabulation(coins, 6)); + assertEquals(-1, CoinChange.greedy(new int[] {5, 3}, 7)); + + int[] two = {2}; + assertEquals(-1, CoinChange.tabulation(two, 3)); + assertEquals(Integer.MIN_VALUE, CoinChange.tabulationOverflowBug(two, 3)); + + long[] naiveCalls = {0}, memoCalls = {0}; + int[] cc = {1, 2, 5}; + int amount = 27; + int a = CoinChange.naive(cc, amount, naiveCalls); + int b = CoinChange.memo(cc, amount, memoCalls); + assertEquals(a, b); + assertEquals(a, CoinChange.tabulation(cc, amount)); + assertTrue(memoCalls[0] <= (long) amount * cc.length + 1); + assertTrue(naiveCalls[0] > memoCalls[0] * 100); + + long combos = CoinChange.countCombinations(cc, 5); + long perms = CoinChange.countPermutations(cc, 5); + assertEquals(4, combos); + assertEquals(9, perms); + + Transcript t = Transcript.start("04-coin-change.txt", "Coin Change: greedy, the overflow bug, call counts, loop order"); + t.line("1) Greedy fails: coins {1,3,4}, amount 6"); + t.line(" greedy -> " + CoinChange.greedy(coins, 6) + " coins (4+1+1)"); + t.line(" DP (table) -> " + CoinChange.tabulation(coins, 6) + " coins (3+3)"); + t.blank(); + t.line("2) 'Impossible' = Integer.MAX_VALUE, then dp[a-c] + 1: coins {2}, amount 3 (really impossible)"); + t.line(" correct (impossible = amount + 1) -> " + CoinChange.tabulation(two, 3)); + t.line(" Integer.MAX_VALUE version -> " + CoinChange.tabulationOverflowBug(two, 3)); + t.blank(); + t.line("3) Calls made: coins {1,2,5}, amount " + amount + ", answer " + a); + t.line(" naive recursion -> " + naiveCalls[0] + " calls"); + t.line(" memoized -> " + memoCalls[0] + " calls"); + t.blank(); + t.line("4) Loop order changes the question: coins {1,2,5}, amount 5"); + t.line(" coins in the OUTER loop -> " + combos + " (combinations: 5 / 2+2+1 / 2+1+1+1 / 1x5)"); + t.line(" amount in the OUTER loop -> " + perms + " (permutations: 1+2 and 2+1 now count separately)"); + t.flush(); + } + + @Test + void knapsack() { + int[] w = {1, 3, 4, 5}, v = {1, 4, 5, 7}; + int[][] dp = Knapsack01.table(w, v, 7); + assertEquals(9, dp[4][7]); + assertEquals(9, Knapsack01.oneRowBackward(w, v, 7)); + + int[] w2 = {5, 4, 6, 3}, v2 = {10, 40, 30, 50}; + assertEquals(90, Knapsack01.tabulation(w2, v2, 10)); + assertEquals(90, Knapsack01.oneRowBackward(w2, v2, 10)); + int bug = Knapsack01.oneRowForwardBug(w2, v2, 10); + assertEquals(150, bug); + + int n = 22; + int[] bw = new int[n], bv = new int[n]; + for (int i = 0; i < n; i++) { bw[i] = 3 + (i * 7) % 11; bv[i] = 10 + (i * 13) % 37; } + long[] naiveCalls = {0}, memoCalls = {0}; + int x = Knapsack01.naive(bw, bv, 60, naiveCalls); + int y = Knapsack01.memo(bw, bv, 60, memoCalls); + assertEquals(x, y); + assertEquals(x, Knapsack01.oneRowBackward(bw, bv, 60)); + assertTrue(memoCalls[0] <= 2L * n * 61 + 1); + + Transcript t = Transcript.start("05-knapsack.txt", "0/1 Knapsack"); + t.line("1) Table: weights [1,3,4,5], values [1,4,5,7], capacity 7. dp[i][w] = best value with the first i items"); + String[] rows = new String[5]; + rows[0] = "none"; + for (int i = 1; i <= 4; i++) rows[i] = "item " + i; + t.line(Tables.ints("w =", rows, Tables.numbers(0, 7), dp)); + t.line("answer = dp[4][7] = " + dp[4][7] + " (weights 3 + 4, values 4 + 5)"); + t.blank(); + t.line("2) One row, capacity loop direction: weights [5,4,6,3], values [10,40,30,50], capacity 10"); + t.line(" backward loop (each item used once) -> " + Knapsack01.oneRowBackward(w2, v2, 10)); + t.line(" forward loop, same code otherwise -> " + bug + " (the weight-3 item is reused three times: 3 x 50)"); + t.blank(); + t.line("3) Calls made: " + n + " items, capacity 60, best value " + x); + t.line(" naive recursion -> " + naiveCalls[0] + " calls"); + t.line(" memoized -> " + memoCalls[0] + " calls"); + t.flush(); + } + + @Test + void lcs() { + String a = "ABCBDAB", b = "BDCABA"; + int[][] dp = Lcs.table(a, b); + assertEquals(4, dp[a.length()][b.length()]); + String sub = Lcs.reconstruct(a, b, dp); + assertEquals(4, sub.length()); + assertTrue(isSubsequence(sub, a) && isSubsequence(sub, b)); + assertEquals(4, Lcs.twoRows(a, b)); + + String p = "ACGTTGCAACGTGCA", q = "TGCATGCCGATGACG"; + long[] naiveCalls = {0}, memoCalls = {0}; + int x = Lcs.naive(p, q, naiveCalls); + int y = Lcs.memo(p, q, memoCalls); + assertEquals(x, y); + assertEquals(x, Lcs.twoRows(p, q)); + assertTrue(memoCalls[0] <= 2L * (p.length() + 1) * (q.length() + 1) + 1); + + Transcript t = Transcript.start("06-lcs.txt", "Longest Common Subsequence: \"" + a + "\" and \"" + b + "\""); + t.line(Tables.ints("", Tables.chars(a), Tables.chars(b), dp)); + t.blank(); + t.line("length (bottom-right cell) = " + dp[a.length()][b.length()]); + t.line("reconstructed from the full table = \"" + sub + "\" (a valid subsequence of both strings)"); + t.line("two-row version = " + Lcs.twoRows(a, b) + " (length only; cannot rebuild the string)"); + t.blank(); + t.line("Calls made on two 15-character strings \"" + p + "\" and \"" + q + "\" (answer " + x + "):"); + t.line(" naive recursion -> " + naiveCalls[0] + " calls"); + t.line(" memoized -> " + memoCalls[0] + " calls"); + t.flush(); + } + + @Test + void editDistance() { + String a = "kitten", b = "sitting"; + int[][] dp = EditDistance.table(a, b); + assertEquals(3, dp[a.length()][b.length()]); + assertEquals(3, EditDistance.twoRows(a, b)); + assertEquals(3, EditDistance.memo("horse", "ros")); + assertEquals(5, EditDistance.tabulation("intention", "execution")); + + Transcript t = Transcript.start("07-edit-distance.txt", "Edit Distance: \"" + a + "\" -> \"" + b + "\""); + t.line(Tables.ints("", Tables.chars(a), Tables.chars(b), dp)); + t.blank(); + t.line("answer = " + dp[a.length()][b.length()] + " (k->s, e->i, insert g)"); + t.line("two-row version = " + EditDistance.twoRows(a, b)); + t.line("horse -> ros = " + EditDistance.tabulation("horse", "ros")); + t.line("intention -> execution = " + EditDistance.tabulation("intention", "execution")); + t.flush(); + } + + @Test + void lis() { + int[] a = {10, 9, 2, 5, 3, 7, 101, 18}; + int[] dp = LongestIncreasingSubsequence.table(a); + assertArrayEquals(new int[] {1, 1, 1, 2, 2, 3, 4, 4}, dp); + assertEquals(4, LongestIncreasingSubsequence.patience(a)); + assertEquals(4, LongestIncreasingSubsequence.memo(a)); + + Transcript t = Transcript.start("08-lis.txt", "Longest Increasing Subsequence: " + Arrays.toString(a)); + t.line("a[i] " + Arrays.toString(a)); + t.line("dp[i] " + Arrays.toString(dp) + " (dp[i] = longest increasing subsequence ENDING at index i)"); + t.blank(); + t.line("answer = max(dp) = " + Arrays.stream(dp).max().getAsInt() + " (for example 2, 3, 7, 18)"); + t.line("O(n log n) patience version = " + LongestIncreasingSubsequence.patience(a)); + t.flush(); + } + + @Test + void uniquePaths() { + long[][] dp = UniquePaths.table(3, 7); + assertEquals(28, dp[2][6]); + assertEquals(48620, UniquePaths.tabulation(10, 10)); + assertEquals(48620, UniquePaths.binomial(10, 10)); + + Transcript t = Transcript.start("09-unique-paths.txt", "Unique Paths on a 3 x 7 grid (right and down only)"); + t.line(Tables.longs("", new String[] {"row 0", "row 1", "row 2"}, Tables.numbers(0, 6), dp)); + t.blank(); + t.line("answer = bottom-right cell = " + dp[2][6]); + t.line("10 x 10 grid: table = " + UniquePaths.tabulation(10, 10) + ", one row = " + UniquePaths.oneRow(10, 10) + + ", binomial C(18,9) = " + UniquePaths.binomial(10, 10)); + t.flush(); + } + + @Test + void partition() { + int[] yes = {1, 5, 11, 5}, no = {1, 2, 3, 5}, odd = {1, 2, 4}; + assertTrue(PartitionEqualSubsetSum.tabulation(yes)); + assertTrue(PartitionEqualSubsetSum.oneRow(yes)); + assertFalse(PartitionEqualSubsetSum.tabulation(no)); + assertFalse(PartitionEqualSubsetSum.tabulation(odd)); + boolean[][] dp = PartitionEqualSubsetSum.table(yes, 11); + assertTrue(dp[4][11]); + + Transcript t = Transcript.start("10-partition.txt", "Partition Equal Subset Sum: " + Arrays.toString(yes) + ", target 11"); + String[] rows = {"none", "+1", "+5", "+11", "+5"}; + t.line("T = some subset of the first i numbers sums to s"); + t.line(Tables.bools("s =", rows, Tables.numbers(0, 11), dp)); + t.blank(); + t.line(Arrays.toString(yes) + " -> " + PartitionEqualSubsetSum.tabulation(yes) + " (5 + 5 + 1 = 11 = 11)"); + t.line(Arrays.toString(no) + " -> " + PartitionEqualSubsetSum.tabulation(no) + " (total 11 is odd)"); + t.line(Arrays.toString(odd) + " -> " + PartitionEqualSubsetSum.tabulation(odd) + " (total 7 is odd)"); + t.flush(); + } + + private static boolean isSubsequence(String s, String of) { + int i = 0; + for (int j = 0; j < of.length() && i < s.length(); j++) if (of.charAt(j) == s.charAt(i)) i++; + return i == s.length(); + } +} diff --git a/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Tables.java b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Tables.java new file mode 100644 index 0000000..82d895e --- /dev/null +++ b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Tables.java @@ -0,0 +1,47 @@ +package com.ankurm.interviewlab.dp.support; + +/** Formats a DP table with row and column labels, for the transcripts. */ +public final class Tables { + private Tables() {} + + public static String ints(String corner, String[] rowLabels, String[] colLabels, int[][] cells) { + return render(corner, rowLabels, colLabels, cells.length, cells[0].length, (r, c) -> String.valueOf(cells[r][c])); + } + + public static String longs(String corner, String[] rowLabels, String[] colLabels, long[][] cells) { + return render(corner, rowLabels, colLabels, cells.length, cells[0].length, (r, c) -> String.valueOf(cells[r][c])); + } + + public static String bools(String corner, String[] rowLabels, String[] colLabels, boolean[][] cells) { + return render(corner, rowLabels, colLabels, cells.length, cells[0].length, (r, c) -> cells[r][c] ? "T" : "."); + } + + /** "" followed by each character of s, so the labels line up with the (n+1)-wide DP table. */ + public static String[] chars(String s) { + String[] out = new String[s.length() + 1]; + out[0] = "-"; + for (int i = 0; i < s.length(); i++) out[i + 1] = String.valueOf(s.charAt(i)); + return out; + } + + public static String[] numbers(int from, int toInclusive) { + String[] out = new String[toInclusive - from + 1]; + for (int i = 0; i < out.length; i++) out[i] = String.valueOf(from + i); + return out; + } + + private interface Cell { String at(int r, int c); } + + private static String render(String corner, String[] rows, String[] cols, int nr, int nc, Cell cell) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%-8s", corner)); + for (int c = 0; c < nc; c++) sb.append(String.format("%4s", cols[c])); + sb.append('\n'); + for (int r = 0; r < nr; r++) { + sb.append(String.format("%-8s", rows[r])); + for (int c = 0; c < nc; c++) sb.append(String.format("%4s", cell.at(r, c))); + sb.append('\n'); + } + return sb.toString().stripTrailing(); + } +} diff --git a/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Transcript.java b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Transcript.java new file mode 100644 index 0000000..7871e99 --- /dev/null +++ b/dynamic-programming/src/test/java/com/ankurm/interviewlab/dp/support/Transcript.java @@ -0,0 +1,43 @@ +package com.ankurm.interviewlab.dp.support; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** Writes output/NN-*.txt while the calling test asserts the same numbers it prints. */ +public final class Transcript { + private final Path path; + private final List lines = new ArrayList<>(); + + private Transcript(Path path, String header) { + this.path = path; + lines.add(header); + lines.add("=".repeat(Math.min(header.length(), 100))); + lines.add(""); + } + + public static Transcript start(String fileName, String header) { + Path dir = Path.of("output"); + try { + Files.createDirectories(dir); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return new Transcript(dir.resolve(fileName), header); + } + + public Transcript line(String text) { lines.add(text); return this; } + public Transcript blank() { lines.add(""); return this; } + + public void flush() { + try { + Files.write(path, (String.join("\n", lines) + "\n").getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +}