Add dynamic-programming module: ten classic DP problems, memoization vs tabulation vs space-optimised, call-count and pitfall transcripts (11 tests)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.ankurm.interviewlab</groupId>
|
||||
<artifactId>dynamic-programming</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>java-interview-lab :: dynamic-programming</name>
|
||||
<description>
|
||||
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.
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>25</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>6.1.3</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-launcher</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<configuration>
|
||||
<argLine>-Xmx512m -Xss1m -Duser.timezone=UTC -Dfile.encoding=UTF-8</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+44
@@ -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
|
||||
Executable
+11
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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()];
|
||||
}
|
||||
}
|
||||
@@ -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<Integer, Long> 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<Integer, Long> 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<Integer, Long> newCache() { return new HashMap<>(); }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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()];
|
||||
}
|
||||
}
|
||||
+59
@@ -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;
|
||||
}
|
||||
}
|
||||
+58
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Integer, Long> 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<Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user