Memoization vs Tabulation — Practice Tasks¶
All tasks must be solved in Go, Java, and Python. Each task ships with a precise spec and starter code in all three languages. Implement both styles where asked and confirm they agree. Always verify memoization and tabulation produce identical results on small inputs before trusting either.
Beginner Tasks (5)¶
Task 1 — Fibonacci three ways¶
Problem. Implement fib(n) three ways: naive recursion (no cache), memoization, and tabulation. Confirm all three agree for 0 ≤ n ≤ 30.
Input / Output spec. - Read n. - Print the three results space-separated (they must be equal).
Constraints. - 0 ≤ n ≤ 40 (naive recursion is fine up to ~40). - fib(0)=0, fib(1)=1.
Hint. Memoization = naive recursion + a cache check at the top and a write before returning. Tabulation = an array filled left to right.
Starter — Go.
package main
import "fmt"
func fibNaive(n int) int {
// TODO
return 0
}
func fibMemo(n int, cache map[int]int) int {
// TODO
return 0
}
func fibTab(n int) int {
// TODO
return 0
}
func main() {
var n int
fmt.Scan(&n)
fmt.Println(fibNaive(n), fibMemo(n, map[int]int{}), fibTab(n))
}
Starter — Java.
import java.util.*;
public class Task1 {
static int fibNaive(int n) { return 0; /* TODO */ }
static int fibMemo(int n, Map<Integer,Integer> c) { return 0; /* TODO */ }
static int fibTab(int n) { return 0; /* TODO */ }
public static void main(String[] a) {
int n = new Scanner(System.in).nextInt();
System.out.println(fibNaive(n)+" "+fibMemo(n,new HashMap<>())+" "+fibTab(n));
}
}
Starter — Python.
def fib_naive(n):
pass # TODO
def fib_memo(n, cache=None):
pass # TODO
def fib_tab(n):
pass # TODO
if __name__ == "__main__":
n = int(input())
print(fib_naive(n), fib_memo(n), fib_tab(n))
Task 2 — Climbing stairs (1 or 2 steps)¶
Problem. Count distinct ways to climb n stairs taking 1 or 2 steps. Return the count modulo 10^9+7.
Input / Output spec. Read n; print ways(n) mod (10^9+7).
Constraints. 0 ≤ n ≤ 10^6. Must be stack-safe → use tabulation, ideally two rolling variables.
Hint. ways(n) = ways(n-1) + ways(n-2), ways(0)=ways(1)=1. Keep two variables, not an array.
Starter — Go.
package main
import "fmt"
const MOD = 1_000_000_007
func climb(n int) int64 {
// TODO: rolling two-variable tabulation
return 0
}
func main() { var n int; fmt.Scan(&n); fmt.Println(climb(n)) }
Starter — Java.
import java.util.*;
public class Task2 {
static final long MOD = 1_000_000_007L;
static long climb(int n) { return 0; /* TODO */ }
public static void main(String[] a) {
System.out.println(climb(new Scanner(System.in).nextInt()));
}
}
Starter — Python.
MOD = 1_000_000_007
def climb(n):
pass # TODO: rolling two-variable tabulation
if __name__ == "__main__":
print(climb(int(input())))
Task 3 — Unique grid paths¶
Problem. Count distinct right/down paths in an R×C grid from top-left to bottom-right.
Input / Output spec. Read R and C; print the path count (fits in 64-bit for R,C ≤ 30).
Constraints. 1 ≤ R, C ≤ 30.
Hint. paths(r,c) = paths(r-1,c) + paths(r,c-1), paths(0,0)=1. A single rolling row gives O(C) space.
Starter — Go.
package main
import "fmt"
func uniquePaths(R, C int) int64 {
// TODO: rolling-row tabulation
return 0
}
func main() { var R, C int; fmt.Scan(&R, &C); fmt.Println(uniquePaths(R, C)) }
Starter — Java.
import java.util.*;
public class Task3 {
static long uniquePaths(int R, int C) { return 0; /* TODO */ }
public static void main(String[] a) {
Scanner s = new Scanner(System.in);
System.out.println(uniquePaths(s.nextInt(), s.nextInt()));
}
}
Starter — Python.
def unique_paths(R, C):
pass # TODO: rolling-row tabulation
if __name__ == "__main__":
R, C = map(int, input().split())
print(unique_paths(R, C))
Task 4 — Memoize an existing slow function¶
Problem. You are given a correct but exponential recursion for the number of ways to tile a 2×n board with dominoes (f(n)=f(n-1)+f(n-2), f(0)=1, f(1)=1). Add memoization without changing the recurrence's shape; confirm it now runs for n = 90 instantly.
Input / Output spec. Read n; print f(n) (use 64-bit; values fit for n ≤ 90).
Constraints. 0 ≤ n ≤ 90.
Hint. Three edits: cache parameter, early return on hit, write before return.
Starter — Go.
package main
import "fmt"
// Given (slow): func f(n int) int64 { if n<2 {return 1}; return f(n-1)+f(n-2) }
func f(n int, cache map[int]int64) int64 {
// TODO: memoize
return 0
}
func main() { var n int; fmt.Scan(&n); fmt.Println(f(n, map[int]int64{})) }
Starter — Java.
import java.util.*;
public class Task4 {
static long f(int n, Map<Integer,Long> cache) { return 0; /* TODO */ }
public static void main(String[] a) {
System.out.println(f(new Scanner(System.in).nextInt(), new HashMap<>()));
}
}
Starter — Python.
def f(n, cache=None):
pass # TODO: memoize the recurrence f(n)=f(n-1)+f(n-2), f(0)=f(1)=1
if __name__ == "__main__":
print(f(int(input())))
Task 5 — Min-cost grid path¶
Problem. Given an R×C grid of non-negative integers, find the minimum sum along a right/down path from top-left to bottom-right.
Input / Output spec. Read R, C, then R rows of C integers; print the minimum path sum.
Constraints. 1 ≤ R, C ≤ 200, cell values 0 ≤ v ≤ 1000.
Hint. dp[r][c] = cost[r][c] + min(dp[r-1][c], dp[r][c-1]). First row/column accumulate. Rolling row gives O(C) space.
Starter — Go.
package main
import "fmt"
func minPath(cost [][]int) int {
// TODO
return 0
}
func main() {
var R, C int
fmt.Scan(&R, &C)
cost := make([][]int, R)
for i := range cost {
cost[i] = make([]int, C)
for j := range cost[i] { fmt.Scan(&cost[i][j]) }
}
fmt.Println(minPath(cost))
}
Starter — Java.
import java.util.*;
public class Task5 {
static int minPath(int[][] cost) { return 0; /* TODO */ }
public static void main(String[] a) {
Scanner s = new Scanner(System.in);
int R = s.nextInt(), C = s.nextInt();
int[][] cost = new int[R][C];
for (int i = 0; i < R; i++) for (int j = 0; j < C; j++) cost[i][j] = s.nextInt();
System.out.println(minPath(cost));
}
}
Starter — Python.
def min_path(cost):
pass # TODO
if __name__ == "__main__":
R, C = map(int, input().split())
cost = [list(map(int, input().split())) for _ in range(R)]
print(min_path(cost))
Intermediate Tasks (4)¶
Task 6 — Convert memoization to tabulation¶
Problem. Given a memoized "coin change — count ways" solver (count the number of ways to make amount using unlimited coins from a list), write the equivalent bottom-up tabulation and assert both agree for several inputs.
Input / Output spec. Read amount, then a line of coin denominations; print the number of combinations (order does not matter), printed twice (memo, then tab) to confirm equality.
Constraints. 0 ≤ amount ≤ 5000, 1 ≤ #coins ≤ 50, coin value ≤ 5000. Use 64-bit.
Hint. Memo state (i, rem) = ways using coins i.. to make rem. Tabulation: dp[a] += dp[a-coin] looping coins outer, amount inner (order matters to avoid counting permutations).
Starter — Go.
package main
import "fmt"
func waysMemo(coins []int, amount int) int64 {
// TODO: memo on (index, remaining)
return 0
}
func waysTab(coins []int, amount int) int64 {
// TODO: dp over amounts, coins outer loop
return 0
}
func main() {
var amount, k int
fmt.Scan(&amount, &k)
coins := make([]int, k)
for i := range coins { fmt.Scan(&coins[i]) }
fmt.Println(waysMemo(coins, amount), waysTab(coins, amount))
}
Starter — Java.
import java.util.*;
public class Task6 {
static long waysMemo(int[] coins, int amount) { return 0; /* TODO */ }
static long waysTab(int[] coins, int amount) { return 0; /* TODO */ }
public static void main(String[] a) {
Scanner s = new Scanner(System.in);
int amount = s.nextInt(), k = s.nextInt();
int[] coins = new int[k];
for (int i = 0; i < k; i++) coins[i] = s.nextInt();
System.out.println(waysMemo(coins, amount) + " " + waysTab(coins, amount));
}
}
Starter — Python.
def ways_memo(coins, amount):
pass # TODO: memo on (index, remaining)
def ways_tab(coins, amount):
pass # TODO: dp over amounts, coins outer loop
if __name__ == "__main__":
amount, _k = map(int, input().split())
coins = list(map(int, input().split()))
print(ways_memo(coins, amount), ways_tab(coins, amount))
Task 7 — Rolling-array space reduction¶
Problem. Compute the number of unique paths in an R×C grid with some blocked cells (1 = blocked). Use a single rolling row (O(C) space). Blocked cells contribute 0.
Input / Output spec. Read R, C, then the grid (0 open, 1 blocked); print path count from (0,0) to (R-1,C-1).
Constraints. 1 ≤ R, C ≤ 1000. Use 64-bit. If start or end is blocked, answer is 0.
Hint. row[c] = 0 if blocked, else row[c] += row[c-1] (with row[c] carrying the "up" value). Initialize the first row carefully, stopping at the first block.
Starter — Go.
package main
import "fmt"
func uniquePathsBlocked(g [][]int) int64 {
// TODO: rolling row, blocked cells = 0
return 0
}
func main() {
var R, C int
fmt.Scan(&R, &C)
g := make([][]int, R)
for i := range g {
g[i] = make([]int, C)
for j := range g[i] { fmt.Scan(&g[i][j]) }
}
fmt.Println(uniquePathsBlocked(g))
}
Starter — Java.
import java.util.*;
public class Task7 {
static long uniquePathsBlocked(int[][] g) { return 0; /* TODO */ }
public static void main(String[] a) {
Scanner s = new Scanner(System.in);
int R = s.nextInt(), C = s.nextInt();
int[][] g = new int[R][C];
for (int i = 0; i < R; i++) for (int j = 0; j < C; j++) g[i][j] = s.nextInt();
System.out.println(uniquePathsBlocked(g));
}
}
Starter — Python.
def unique_paths_blocked(g):
pass # TODO: rolling row, blocked = 0
if __name__ == "__main__":
R, C = map(int, input().split())
g = [list(map(int, input().split())) for _ in range(R)]
print(unique_paths_blocked(g))
Task 8 — Climbing stairs with a custom step set¶
Problem. Count ways to climb n stairs given an allowed step-set S (e.g. {1,3,5}). Return the count mod 10^9+7. Provide both memoized and tabulated solutions.
Input / Output spec. Read n, then a line of allowed step sizes; print the count both ways (equal).
Constraints. 0 ≤ n ≤ 10^5, 1 ≤ |S| ≤ 10, step sizes ≤ n.
Hint. ways(n) = Σ_{s∈S} ways(n-s), ways(0)=1, ways(<0)=0. Watch recursion depth in the memo version for large n.
Starter — Go.
package main
import "fmt"
const MOD = 1_000_000_007
func climbMemo(n int, steps []int, cache map[int]int64) int64 {
// TODO
return 0
}
func climbTab(n int, steps []int) int64 {
// TODO
return 0
}
func main() {
var n, k int
fmt.Scan(&n, &k)
steps := make([]int, k)
for i := range steps { fmt.Scan(&steps[i]) }
fmt.Println(climbMemo(n, steps, map[int]int64{}), climbTab(n, steps))
}
Starter — Java.
import java.util.*;
public class Task8 {
static final long MOD = 1_000_000_007L;
static long climbMemo(int n, int[] steps, Map<Integer,Long> c) { return 0; /* TODO */ }
static long climbTab(int n, int[] steps) { return 0; /* TODO */ }
public static void main(String[] a) {
Scanner s = new Scanner(System.in);
int n = s.nextInt(), k = s.nextInt();
int[] steps = new int[k];
for (int i = 0; i < k; i++) steps[i] = s.nextInt();
System.out.println(climbMemo(n, steps, new HashMap<>()) + " " + climbTab(n, steps));
}
}
Starter — Python.
MOD = 1_000_000_007
def climb_memo(n, steps, cache=None):
pass # TODO
def climb_tab(n, steps):
pass # TODO
if __name__ == "__main__":
n, _k = map(int, input().split())
steps = list(map(int, input().split()))
print(climb_memo(n, steps), climb_tab(n, steps))
Advanced Tasks (3)¶
Task 9 — Stack-safe iterative memoization¶
Problem. Implement a memoized grid-path counter that does not use language recursion (no call-stack growth), using an explicit work stack. It must handle R = C = 5000 without overflowing. Compare against a tabulated reference.
Input / Output spec. Read R, C; print the path count mod 10^9+7 from the iterative-memo solver and the tabulated reference (equal).
Constraints. 1 ≤ R, C ≤ 5000. Reduce mod 10^9+7.
Hint. Two-phase frames: phase 0 pushes children, phase 1 combines their cached values. Use a hash map (or flat array) keyed by (r,c).
Starter — Go.
package main
import "fmt"
const MOD = 1_000_000_007
func gridIterMemo(R, C int) int64 {
// TODO: explicit stack of {r,c,phase}; no recursion
return 0
}
func gridTab(R, C int) int64 {
// TODO: reference rolling-row tabulation, mod MOD
return 0
}
func main() { var R, C int; fmt.Scan(&R, &C); fmt.Println(gridIterMemo(R, C), gridTab(R, C)) }
Starter — Java.
import java.util.*;
public class Task9 {
static final long MOD = 1_000_000_007L;
static long gridIterMemo(int R, int C) { return 0; /* TODO */ }
static long gridTab(int R, int C) { return 0; /* TODO */ }
public static void main(String[] a) {
Scanner s = new Scanner(System.in);
int R = s.nextInt(), C = s.nextInt();
System.out.println(gridIterMemo(R, C) + " " + gridTab(R, C));
}
}
Starter — Python.
MOD = 1_000_000_007
def grid_iter_memo(R, C):
pass # TODO: explicit stack of (r,c,phase); no recursion
def grid_tab(R, C):
pass # TODO: reference rolling-row tabulation
if __name__ == "__main__":
R, C = map(int, input().split())
print(grid_iter_memo(R, C), grid_tab(R, C))
Task 10 — Reconstruct the optimal path¶
Problem. For the min-cost grid path, return not just the minimum cost but the actual sequence of moves (R/D). This requires the full table or parent pointers — a rolling array will not suffice. Explain in a comment why.
Input / Output spec. Read R, C, then the cost grid; print the min cost, then the move string (e.g. RRDD).
Constraints. 1 ≤ R, C ≤ 500, costs 0 ≤ v ≤ 1000.
Hint. Fill the full dp table, then walk from (R-1,C-1) to (0,0) choosing the smaller predecessor at each step; reverse the recorded moves.
Starter — Go.
package main
import "fmt"
func minPathWithRoute(cost [][]int) (int, string) {
// TODO: full dp table + backtrack
return 0, ""
}
func main() {
var R, C int
fmt.Scan(&R, &C)
cost := make([][]int, R)
for i := range cost {
cost[i] = make([]int, C)
for j := range cost[i] { fmt.Scan(&cost[i][j]) }
}
c, route := minPathWithRoute(cost)
fmt.Println(c)
fmt.Println(route)
}
Starter — Java.
import java.util.*;
public class Task10 {
// returns {cost, route} packed; implement as you prefer
static Object[] minPathWithRoute(int[][] cost) { return new Object[]{0, ""}; /* TODO */ }
public static void main(String[] a) {
Scanner s = new Scanner(System.in);
int R = s.nextInt(), C = s.nextInt();
int[][] cost = new int[R][C];
for (int i = 0; i < R; i++) for (int j = 0; j < C; j++) cost[i][j] = s.nextInt();
Object[] res = minPathWithRoute(cost);
System.out.println(res[0]);
System.out.println(res[1]);
}
}
Starter — Python.
def min_path_with_route(cost):
pass # TODO: full dp table + backtrack; return (cost, route)
if __name__ == "__main__":
R, C = map(int, input().split())
cost = [list(map(int, input().split())) for _ in range(R)]
c, route = min_path_with_route(cost)
print(c)
print(route)
Task 11 — Equivalence test harness¶
Problem. Write a test harness that, for a chosen DP (e.g. climbing stairs with a random step set), generates random small inputs, runs both the memoized and tabulated solvers, and asserts they always agree. Report the number of cases passed. This codifies the central lesson: the two styles must produce identical answers.
Input / Output spec. Read a seed and a number of trials T; for each trial generate random n and step-set, compare both solvers, and finally print OK <T> if all agree (or the first mismatch).
Constraints. 1 ≤ T ≤ 10^4, generated n ≤ 2000.
Hint. A mismatch means a bug in one solver (base case, fill order, or state key). Keep the generators deterministic from the seed for reproducibility.
Starter — Go.
package main
import (
"fmt"
"math/rand"
)
func climbMemo(n int, steps []int, c map[int]int64) int64 { return 0 /* reuse Task 8 */ }
func climbTab(n int, steps []int) int64 { return 0 /* reuse Task 8 */ }
func main() {
var seed int64
var T int
fmt.Scan(&seed, &T)
r := rand.New(rand.NewSource(seed))
for t := 0; t < T; t++ {
n := r.Intn(2000)
// TODO: random step set; compare both; report first mismatch
_ = n
}
fmt.Printf("OK %d\n", T)
}
Starter — Java.
import java.util.*;
public class Task11 {
static long climbMemo(int n, int[] s, Map<Integer,Long> c) { return 0; /* reuse */ }
static long climbTab(int n, int[] s) { return 0; /* reuse */ }
public static void main(String[] a) {
Scanner sc = new Scanner(System.in);
long seed = sc.nextLong(); int T = sc.nextInt();
Random r = new Random(seed);
for (int t = 0; t < T; t++) {
int n = r.nextInt(2000);
// TODO: random step set; compare; report mismatch
}
System.out.println("OK " + T);
}
}
Starter — Python.
import random
def climb_memo(n, steps, cache=None):
pass # reuse Task 8
def climb_tab(n, steps):
pass # reuse Task 8
if __name__ == "__main__":
seed, T = map(int, input().split())
rng = random.Random(seed)
for _ in range(T):
n = rng.randint(0, 2000)
# TODO: random step set; compare both; report first mismatch
print("OK", T)
Evaluation Criteria¶
| Criterion | Beginner | Intermediate | Advanced |
|---|---|---|---|
| Correct base cases | required | required | required |
| Both styles agree | shown | asserted | harness-tested |
| Complexity stated | states × transition | with space analysis | with depth/memory budget |
| Space optimization | rolling where asked | rolling row | full table when path needed |
| Robustness | handles n=0, 1×1 grid | mod / overflow safe | stack-safe at scale |
Work each task in all three languages, run the provided main, and confirm the memoized and tabulated outputs match before considering the task done.