Each problem below is stated, given a state definition and recurrence, and then
shown as a fully worked DP table for one concrete example — the same table you would fill
by hand on a whiteboard.
Problem set follows the running order of the
Dynamic Programming playlist by Tushar Roy
(all 42 videos). Every table on this page was computed by running the algorithm and checked against its
known answer — nothing was typed by hand. Explanations and code are original; the playlist is the index.
All implementations are C++ and assume #include <bits/stdc++.h> and
using namespace std;. Each one was compiled with g++ -std=c++17 -Wall and run
against the worked example shown on its card.
Given n items each with a weight and a value, and a knapsack of capacity W, pick a subset of items with total weight ≤ W that maximises total value. Each item may be taken at most once (that is the “0/1” part — no fractions, no repeats).
dp[i][c] — rows are items added one at a time, columns are capacity
item \ cap
0
1
2
3
4
5
6
7
– (none)
0
0
0
0
0
0
0
0
w=1, v=1
0
1
1
1
1
1
1
1
w=3, v=4
0
1
1
4
5
5
5
5
w=4, v=5
0
1
1
4
5
6
6
9
w=5, v=7
0
1
1
4
5
7
8
9
Gold = final answer. Blue = cells on the traceback path (the chosen items).
Answer: dp[4][7] = 9 (items of weight 3 and 4 → value 4 + 5)
TimeO(n·W)SpaceO(n·W), reducible to O(W)
Implementation (C++)
int knapsack(const vector<int>& w, const vector<int>& v, int W) {
int n = w.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; ++i)
for (int c = 0; c <= W; ++c) {
dp[i][c] = dp[i-1][c]; // skip item i
if (w[i-1] <= c) // or take it
dp[i][c] = max(dp[i][c], v[i-1] + dp[i-1][c - w[i-1]]);
}
return dp[n][W];
}
Given two strings, find the length of the longest sequence of characters that appears in both, in the same relative order. The characters need not be contiguous.
Worked example
a = "ABCDGH", b = "AEDFHR"
State
dp[i][j] = LCS length of the first i characters of a and the first j characters of b.
Given a set of positive integers and a target T, decide whether some subset adds up to exactly T. Same shape as knapsack, but the table holds booleans.
Worked example
set = [2, 3, 7, 8, 10], target T = 11
State
dp[i][t] = true if some subset of the first i numbers sums to exactly t.
Recurrence
dp[i][0] = true (the empty subset), dp[0][t>0] = false
dp[i][t] = dp[i-1][t] if s[i] > t
dp[i][t] = dp[i-1][t] OR dp[i-1][t-s[i]] otherwise
DP table
dp[i][t] — T = reachable, F = not reachable
num \ sum
0
1
2
3
4
5
6
7
8
9
10
11
– (none)
T
F
F
F
F
F
F
F
F
F
F
F
2
T
F
T
F
F
F
F
F
F
F
F
F
3
T
F
T
T
F
T
F
F
F
F
F
F
7
T
F
T
T
F
T
F
T
F
T
T
F
8
T
F
T
T
F
T
F
T
T
T
T
T
10
T
F
T
T
F
T
F
T
T
T
T
T
Green = reachable, grey = not. Gold = target. Blue = the numbers picked on the traceback.
Answer: dp[5][11] = true (the traceback recovers the subset {3, 8})
TimeO(n·T)SpaceO(n·T), reducible to O(T)
Implementation (C++)
bool subsetSum(const vector<int>& s, int T) {
int n = s.size();
vector<vector<char>> dp(n + 1, vector<char>(T + 1, false));
for (int i = 0; i <= n; ++i)
dp[i][0] = true; // empty subset hits 0
for (int i = 1; i <= n; ++i)
for (int t = 1; t <= T; ++t) {
dp[i][t] = dp[i-1][t];
if (s[i-1] <= t && dp[i-1][t - s[i-1]])
dp[i][t] = true;
}
return dp[n][T];
}
Given coin denominations of unlimited supply, make an amount A using as few coins as possible. Greedy fails here — with coins [1,5,6,9] and A = 11 greedy takes 9+1+1 = 3 coins, but 5+6 = 2 is optimal.
Worked example
coins = [1, 5, 6, 9], amount A = 11
State
dp[i][a] = fewest coins to make amount a using only the first i denominations.
Recurrence
dp[i][0] = 0, dp[0][a>0] = ∞
dp[i][a] = dp[i-1][a] if coin[i] > a
dp[i][a] = min( dp[i-1][a], 1 + dp[i][a-coin[i]] ) otherwise
skip coin reuse the same coin (row i, not i-1)
DP table
dp[i][a] — note the second term reads from the same row, which is what makes coins reusable
coin \ amt
0
1
2
3
4
5
6
7
8
9
10
11
– (none)
0
∞
∞
∞
∞
∞
∞
∞
∞
∞
∞
∞
1
0
1
2
3
4
5
6
7
8
9
10
11
5
0
1
2
3
4
1
2
3
4
5
2
3
6
0
1
2
3
4
1
1
2
3
4
2
2
9
0
1
2
3
4
1
1
2
3
1
2
2
Gold = answer. Blue = coins used on the traceback. Grey ∞ = unreachable.
Answer: dp[4][11] = 2 (5 + 6)
TimeO(n·A)SpaceO(n·A), reducible to O(A)
Implementation (C++)
int minCoins(const vector<int>& coins, int A) {
const int INF = INT_MAX / 2;
vector<int> dp(A + 1, INF);
dp[0] = 0;
for (int c : coins) // unbounded: amount ascends
for (int a = c; a <= A; ++a)
dp[a] = min(dp[a], dp[a - c] + 1);
return dp[A] >= INF ? -1 : dp[A];
}
Find the length of the longest strictly increasing subsequence of an array. Elements need not be adjacent, but must keep their original order.
Worked example
a = [3, 4, -1, 0, 6, 2, 3]
State
dp[i] = length of the longest increasing subsequence that ends exactly at index i.
Recurrence
dp[i] = 1 + max{ dp[j] : j < i and a[j] < a[i] } (or 1 if no such j)
answer = max over all i of dp[i] — not dp[n-1]
DP table
dp[i] scanned left to right; the third row records which earlier index we extended
row \ index
0
1
2
3
4
5
6
a[i]
3
4
-1
0
6
2
3
dp[i]
1
2
1
2
3
3
4
prev[i]
–
0
–
2
1
3
5
Gold = the maximum (the answer). Blue = the chain of predecessors forming that subsequence.
Answer: max dp = 4 (the subsequence −1, 0, 2, 3)
TimeO(n²) — O(n log n) with patience sortingSpaceO(n)
Implementation (C++)
int lis(const vector<int>& a) {
int n = a.size(), best = 0;
vector<int> dp(n, 1);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j)
if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);
best = max(best, dp[i]);
}
return best;
}
Given a chain of matrices, choose the parenthesisation that minimises the total number of scalar multiplications. Matrix sizes are given as a dimension array p, where matrix Aᵢ is p[i-1] × p[i].
Given sorted keys and how often each is searched for, build the BST that minimises the total expected search cost — that is, Σ freq[k] × (depth of k + 1). Frequently searched keys want to sit near the root.
Worked example
keys = [10, 12, 20], freq = [34, 8, 50]
State
dp[i][j] = minimum cost of an optimal BST built from keys i..j.
Recurrence
dp[i][i] = freq[i]
dp[i][j] = min over r in [i, j] of
dp[i][r-1] + dp[r+1][j] + sum(freq[i..j])
The sum term appears because making key r the root pushes every
key in the range one level deeper, adding its frequency once.
DP table
dp[i][j] — expected search cost for the key range i..j
i \ j
0
1
2
0: key 10
34
50
142
1: key 12
8
66
2: key 20
50
Gold = full-range cost. Diagonal = a single key as its own root.
Answer: dp[0][2] = 142 (root = 20, left child 10, right child of 10 is 12)
TimeO(n³) — O(n²) with Knuth's optimisationSpaceO(n²)
Implementation (C++)
int optimalBST(const vector<int>& keys, const vector<int>& freq) {
int n = keys.size();
vector<int> pre(n + 1, 0);
for (int i = 0; i < n; ++i) pre[i+1] = pre[i] + freq[i];
auto total = [&](int i, int j) { return pre[j+1] - pre[i]; };
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int i = 0; i < n; ++i) dp[i][i] = freq[i];
for (int L = 2; L <= n; ++L)
for (int i = 0; i + L - 1 < n; ++i) {
int j = i + L - 1;
dp[i][j] = INT_MAX;
for (int r = i; r <= j; ++r) {
int left = (r > i) ? dp[i][r-1] : 0;
int right = (r < j) ? dp[r+1][j] : 0;
dp[i][j] = min(dp[i][j], left + right + total(i, j));
}
}
return dp[0][n-1];
}
Find the length of the longest subsequence of a string that reads the same forwards and backwards. (Equivalently: the LCS of the string with its own reverse.)
Worked example
s = "AGBCBA"
State
dp[i][j] = length of the longest palindromic subsequence inside s[i..j].
Recurrence
dp[i][i] = 1
dp[i][j] = dp[i+1][j-1] + 2 if s[i] == s[j]
dp[i][j] = max( dp[i+1][j], dp[i][j-1] ) otherwise
Fill by increasing substring length — dp[i][j] depends on shorter windows.
DP table
dp[i][j] over substrings s[i..j] — fills diagonally outward from the main diagonal
i \ j
0:A
1:G
2:B
3:C
4:B
5:A
0:A
1
1
1
1
3
5
1:G
1
1
1
3
3
2:B
1
1
3
3
3:C
1
1
1
4:B
1
1
5:A
1
Gold = the whole string. Diagonal = 1 (a single character is a palindrome).
Answer: dp[0][5] = 5 (the subsequence "ABCBA")
TimeO(n²)SpaceO(n²)
Implementation (C++)
int lps(const string& s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int i = 0; i < n; ++i) dp[i][i] = 1;
for (int L = 2; L <= n; ++L)
for (int i = 0; i + L - 1 < n; ++i) {
int j = i + L - 1;
if (s[i] == s[j]) dp[i][j] = (L > 2 ? dp[i+1][j-1] : 0) + 2;
else dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
return dp[0][n-1];
}
Decide whether a pattern matches a whole string, where '.' matches any single character and '*' means “zero or more of the preceding element”. The match must cover the entire string.
Worked example
text = "xaabyc", pattern = "xa*b.c"
State
dp[i][j] = true if the first i pattern characters match the first j text characters.
Recurrence
dp[0][0] = true
dp[i][0] = dp[i-2][0] if p[i] == '*' (a '*' group can match nothing)
if p[i] == '.' or p[i] == t[j]: dp[i][j] = dp[i-1][j-1]
if p[i] == '*': dp[i][j] = dp[i-2][j] (use zero copies)
OR dp[i][j-1] when p[i-1] matches t[j] (use one more copy)
otherwise: dp[i][j] = false
DP table
dp[i][j] — rows are the pattern, columns are the text
pat \ text
–
x
a
a
b
y
c
–
T
F
F
F
F
F
F
x
F
T
F
F
F
F
F
a
F
F
T
F
F
F
F
*
F
T
T
T
F
F
F
b
F
F
F
F
T
F
F
.
F
F
F
F
F
T
F
c
F
F
F
F
F
F
T
Green = matches, grey = does not. Gold = full pattern vs full text.
Answer: dp[6][6] = true
TimeO(n·m)SpaceO(n·m)
Implementation (C++)
bool isMatch(const string& t, const string& p) {
int n = t.size(), m = p.size();
vector<vector<char>> dp(m + 1, vector<char>(n + 1, false));
dp[0][0] = true;
for (int i = 1; i <= m; ++i)
if (p[i-1] == '*') dp[i][0] = dp[i-2][0];
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j) {
if (p[i-1] == '*') {
bool same = (p[i-2] == '.' || p[i-2] == t[j-1]);
dp[i][j] = dp[i-2][j] || (same && dp[i][j-1]);
} else if (p[i-1] == '.' || p[i-1] == t[j-1]) {
dp[i][j] = dp[i-1][j-1];
}
}
return dp[m][n];
}
A rod of length N can be cut into integer-length pieces, and a piece of length i sells for price[i]. Choose the cuts that maximise total revenue. This is unbounded knapsack: each piece length can be used any number of times.
Worked example
price[1..8] = [1, 5, 8, 9, 10, 17, 17, 20], rod length N = 8
State
dp[i][L] = best revenue from a rod of length L using only piece lengths 1..i.
Recurrence
dp[i][0] = 0, dp[0][L] = 0
dp[i][L] = dp[i-1][L] if i > L
dp[i][L] = max( dp[i-1][L], price[i] + dp[i][L-i] ) otherwise
same row i → reuse allowed
DP table
dp[i][L] — rows introduce one more usable piece length at a time
piece \ len
0
1
2
3
4
5
6
7
8
– (none)
0
0
0
0
0
0
0
0
0
len 1 → 1
0
1
2
3
4
5
6
7
8
len 2 → 5
0
1
5
6
10
11
15
16
20
len 3 → 8
0
1
5
8
10
13
16
18
21
len 4 → 9
0
1
5
8
10
13
16
18
21
len 5 → 10
0
1
5
8
10
13
16
18
21
len 6 → 17
0
1
5
8
10
13
17
18
22
len 7 → 17
0
1
5
8
10
13
17
18
22
len 8 → 20
0
1
5
8
10
13
17
18
22
Gold = answer. Blue = pieces chosen on the traceback.
Answer: dp[8][8] = 22 (cut into a piece of length 6 and a piece of length 2 → 17 + 5)
TimeO(n·N)SpaceO(n·N), reducible to O(N)
Implementation (C++)
int rodCutting(const vector<int>& price, int N) {
// price[i-1] is the price of a piece of length i
vector<int> dp(N + 1, 0);
for (int L = 1; L <= N; ++L)
for (int i = 1; i <= min<int>(L, price.size()); ++i)
dp[L] = max(dp[L], price[i-1] + dp[L - i]);
return dp[N];
}
Each element of the array says the maximum number of steps you may jump forward from that position. Starting at index 0, reach the last index in as few jumps as possible.
Worked example
a = [2, 3, 1, 1, 2, 4, 2, 0, 1, 1]
State
dp[i] = fewest jumps needed to land on index i.
Recurrence
dp[0] = 0
dp[i] = 1 + min{ dp[j] : j < i and j + a[j] ≥ i }
dp[i] = ∞ if no such j exists (index i is unreachable)
DP table
dp[i] left to right; prev[i] records which index we jumped from
row \ index
0
1
2
3
4
5
6
7
8
9
a[i]
2
3
1
1
2
4
2
0
1
1
dp[i]
0
1
1
2
2
3
3
4
4
4
prev[i]
–
0
0
1
1
4
4
5
5
5
Gold = the last index. Blue = the chain of jumps that reaches it.
Answer: dp[9] = 4 (indices 0 → 1 → 4 → 5 → 9)
TimeO(n²) — O(n) with a greedy window sweepSpaceO(n)
Implementation (C++)
int minJumps(const vector<int>& a) {
int n = a.size();
const int INF = INT_MAX / 2;
vector<int> dp(n, INF);
dp[0] = 0;
for (int i = 1; i < n; ++i)
for (int j = 0; j < i; ++j)
if (j + a[j] >= i) dp[i] = min(dp[i], dp[j] + 1);
return dp[n-1] >= INF ? -1 : dp[n-1];
}
Each job has a start time, an end time and a profit. Pick a set of jobs that do not overlap in time so that total profit is maximised. Unlike the unweighted version, “earliest finishing time first” greedy is wrong once profits differ.
Worked example
jobs (start, end, profit): (1,3,5) (2,5,6) (4,6,5) (5,8,11) (6,7,4) (7,9,2) — sorted by end time
State
dp[i] = maximum profit considering only the first i jobs, after sorting by end time.
Recurrence
Sort jobs by end time. Let latest(i) = the largest index j < i with end[j] ≤ start[i].
dp[0] = profit[0]
dp[i] = max( dp[i-1], profit[i] + dp[latest(i)] )
skip job i take it, then jump past all overlaps
DP table
Jobs sorted by end time; dp is a running best-so-far
row \ index
0
1
2
3
4
5
interval
1–3
2–5
4–6
6–7
5–8
7–9
profit
5
6
5
4
11
2
latest(i)
–
–
0
2
1
3
dp[i]
5
6
10
14
17
17
Gold = final answer. Blue = jobs actually selected on the traceback.
Answer: dp[5] = 17 (job 2–5 with profit 6, then job 5–8 with profit 11)
TimeO(n log n) with binary search for latest(i)SpaceO(n)
Implementation (C++)
struct Job { int start, end, profit; };
int jobScheduling(vector<Job> jobs) {
sort(jobs.begin(), jobs.end(),
[](const Job& x, const Job& y) { return x.end < y.end; });
int n = jobs.size();
vector<int> ends(n), dp(n);
for (int i = 0; i < n; ++i) ends[i] = jobs[i].end;
dp[0] = jobs[0].profit;
for (int i = 1; i < n; ++i) {
// rightmost job whose end <= start of job i
int k = int(upper_bound(ends.begin(), ends.begin() + i,
jobs[i].start) - ends.begin()) - 1;
int incl = jobs[i].profit + (k >= 0 ? dp[k] : 0);
dp[i] = max(dp[i-1], incl);
}
return dp[n-1];
}
Find the contiguous rectangular submatrix with the largest sum. The trick is to fix a pair of top and bottom rows, collapse those rows into a single array of column sums, and run 1D Kadane on it — turning a 2D search into R² runs of a linear scan.
Worked example
the 4×5 matrix below
State
For each (top, bottom) row pair: acc[c] = sum of column c between those rows. Kadane on acc gives the best left/right column pair.
Recurrence
for top in 0..R-1:
acc = [0] * C
for bot in top..R-1:
acc[c] += M[bot][c] for every column c
best = max(best, kadane(acc))
DP table
The input matrix, with the winning submatrix highlighted
row \ col
0
1
2
3
4
0
2
1
-3
-4
5
1
0
6
3
4
1
2
2
-2
-1
4
-5
3
-3
3
1
0
3
Every (top, bottom) row pair and the best Kadane result on its collapsed column sums
top row
bottom row
best strip sum
cols
0
0
0
5
4–4
1
0
1
15
0–4
2
0
2
13
0–4
3
0
3
17
0–4
4
1
1
14
1–4
5
1
2
16
0–3
6
1
3
18
1–3
7
2
2
4
3–3
8
2
3
5
1–3
9
3
3
7
1–4
Gold = the row pair that produced the global maximum.
int maxSumSubmatrix(const vector<vector<int>>& M) {
int R = M.size(), C = M[0].size();
int best = INT_MIN;
for (int top = 0; top < R; ++top) {
vector<int> acc(C, 0);
for (int bot = top; bot < R; ++bot) {
for (int c = 0; c < C; ++c) acc[c] += M[bot][c];
int cur = 0; // Kadane on the collapsed strip
for (int x : acc) {
cur = (cur <= 0) ? x : cur + x;
best = max(best, cur);
}
}
}
return best;
}
There is a floor T in a building such that an egg dropped from floor T or above breaks, and from below T it does not. With a given number of identical eggs, find the minimum number of drops that guarantees identifying T in the worst case.
Worked example
eggs = 2, floors = 10
State
dp[e][f] = worst-case drops needed with e eggs and f floors still in question.
Recurrence
dp[1][f] = f (one egg → must test floor by floor from the bottom)
dp[e][0] = 0, dp[e][1] = 1
dp[e][f] = 1 + min over k in [1, f] of
max( dp[e-1][k-1], dp[e][f-k] )
egg breaks egg survives
The max is the adversary picking the worse branch; the min is you picking the best floor k.
DP table
dp[e][f] — note how the second egg turns a linear scan into roughly √(2f)
eggs \ floors
0
1
2
3
4
5
6
7
8
9
10
1 egg
0
1
2
3
4
5
6
7
8
9
10
2 eggs
0
1
2
2
3
3
3
4
4
4
4
Gold = the answer. Row 1 is just the identity — with one egg you cannot do better than linear.
Answer: dp[2][10] = 4 (first drop from floor 4, then 7, then 9, then 10)
TimeO(e·f²) — O(e·f) with a smarter formulationSpaceO(e·f)
Implementation (C++)
int eggDrop(int eggs, int floors) {
vector<vector<int>> dp(eggs + 1, vector<int>(floors + 1, 0));
for (int f = 0; f <= floors; ++f) dp[1][f] = f;
for (int e = 2; e <= eggs; ++e)
for (int f = 1; f <= floors; ++f) {
dp[e][f] = INT_MAX;
for (int k = 1; k <= f; ++k)
dp[e][f] = min(dp[e][f], 1 + max(dp[e-1][k-1], dp[e][f-k]));
}
return dp[eggs][floors];
}
Find the longest run of characters that appears contiguously in both strings. Almost the same table as LCS, with one crucial difference: a mismatch resets the cell to 0 instead of inheriting a neighbour, because a substring cannot skip characters.
Worked example
a = "ABCDGH", b = "ACDGHR"
State
dp[i][j] = length of the longest common suffix of a[0..i-1] and b[0..j-1].
Recurrence
dp[i][j] = dp[i-1][j-1] + 1 if a[i] == b[j]
dp[i][j] = 0 otherwise ← the difference from LCS
answer = max cell in the whole table (not the bottom-right corner)
DP table
dp[i][j] — every mismatch knocks the cell back to zero, so runs are visible as diagonals
a \ b
–
A
C
D
G
H
R
–
0
0
0
0
0
0
0
A
0
1
0
0
0
0
0
B
0
0
0
0
0
0
0
C
0
0
1
0
0
0
0
D
0
0
0
2
0
0
0
G
0
0
0
0
3
0
0
H
0
0
0
0
0
4
0
Gold = the maximum cell. Blue = the diagonal run leading into it.
Answer: max cell = 4 at dp[6][5] (the substring "CDGH")
TimeO(n·m)SpaceO(n·m), reducible to O(m)
Implementation (C++)
int longestCommonSubstring(const string& a, const string& b) {
int n = a.size(), m = b.size(), best = 0;
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (a[i-1] == b[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
best = max(best, dp[i][j]);
}
// else stays 0 -- the run is broken
return best;
}
Given a string with no spaces and a dictionary, decide whether the string can be segmented into a sequence of dictionary words. The greedy “take the longest word “ approach fails, because an early long match can strand the rest of the string.
Count how many distinct combinations of coins add up to an amount. Combinations, not permutations — 1+2 and 2+1 count once. Adding coins one denomination at a time (outer loop over coins) is exactly what prevents double counting.
Worked example
coins = [1, 2, 3], amount A = 5
State
dp[i][a] = number of ways to make amount a using only the first i denominations.
Recurrence
dp[i][0] = 1 (one way: use nothing), dp[0][a>0] = 0
dp[i][a] = dp[i-1][a] + dp[i][a-coin[i]]
ignore coin i use coin i at least once
DP table
dp[i][a] — each new row folds in one more denomination
coin \ amt
0
1
2
3
4
5
– (none)
1
0
0
0
0
0
1
1
1
1
1
1
1
2
1
1
2
2
3
3
3
1
1
2
3
4
5
Gold = the answer. Column 0 is all 1s — there is exactly one way to make nothing.
long long countWays(const vector<int>& coins, int A) {
vector<long long> dp(A + 1, 0);
dp[0] = 1;
for (int c : coins) // coins outer => combinations, not permutations
for (int a = c; a <= A; ++a)
dp[a] += dp[a - c];
return dp[A];
}
In a binary matrix, find the largest area rectangle containing only 1s. The insight: treat every row as the base of a histogram whose bar heights are the runs of 1s stacked above it, then solve “largest rectangle in a histogram” once per row.
Worked example
the 4×5 binary matrix below
State
h[r][c] = number of consecutive 1s ending at row r in column c. For each row, run the histogram routine on h[r].
Recurrence
h[r][c] = 0 if M[r][c] == 0
h[r][c] = h[r-1][c] + 1 if M[r][c] == 1
answer = max over rows r of largestRectangleInHistogram( h[r] )
DP table
The input matrix, with the winning rectangle highlighted
row \ col
0
1
2
3
4
0
1
0
1
1
1
1
1
0
1
1
1
2
0
1
1
1
1
3
1
0
0
1
0
h[r][c] — the histogram heights that each row sees looking upward
row \ col
0
1
2
3
4
0
1
0
1
1
1
1
2
0
2
2
2
2
0
1
3
3
3
3
1
0
0
4
0
Gold = the histogram bars that formed the winning rectangle.
TimeO(R·C) — the histogram scan is linear with a stackSpaceO(C)
Implementation (C++)
int largestHistogram(const vector<int>& h) {
int n = h.size(), best = 0;
stack<int> st;
for (int i = 0; i <= n; ++i) {
int cur = (i < n) ? h[i] : 0;
while (!st.empty() && h[st.top()] >= cur) {
int top = st.top(); st.pop();
int left = st.empty() ? 0 : st.top() + 1;
best = max(best, h[top] * (i - left));
}
st.push(i);
}
return best;
}
int maximalRectangle(const vector<vector<int>>& M) {
int R = M.size(), C = M[0].size(), best = 0;
vector<int> h(C, 0);
for (int r = 0; r < R; ++r) {
for (int c = 0; c < C; ++c) h[c] = M[r][c] ? h[c] + 1 : 0;
best = max(best, largestHistogram(h));
}
return best;
}
The same minimum-coin problem as #05, collapsed to a single array. Instead of a row per denomination, walk the amount from 1 upward and try every coin at each step. This is the form worth memorising — it is shorter, uses O(A) space, and generalises cleanly.
Worked example
coins = [7, 2, 3, 6], amount A = 13
State
dp[a] = fewest coins to make exactly amount a, using any denomination any number of times.
Recurrence
dp[0] = 0, dp[a] = ∞ initially
dp[a] = 1 + min{ dp[a - c] : c in coins, c ≤ a }
Amounts are filled ascending, so dp[a-c] is always already final.
DP table
dp[a] filled left to right; the last row records the coin that produced each minimum
row \ amount
0
1
2
3
4
5
6
7
8
9
10
11
12
13
amount
0
1
2
3
4
5
6
7
8
9
10
11
12
13
dp[a]
0
∞
1
1
2
2
1
1
2
2
2
3
2
2
coin used
–
–
2
3
2
2
6
7
2
7
7
7
6
7
Gold = the answer. Blue = amounts visited on the traceback.
Answer: dp[13] = 2 (7 + 6)
TimeO(n·A)SpaceO(A)
Implementation (C++)
int minCoins(const vector<int>& coins, int A) {
const int INF = INT_MAX / 2;
vector<int> dp(A + 1, INF);
dp[0] = 0;
for (int a = 1; a <= A; ++a)
for (int c : coins)
if (c <= a && dp[a - c] + 1 < dp[a])
dp[a] = dp[a - c] + 1;
return dp[A] >= INF ? -1 : dp[A];
}
Given daily prices, make at most K buy-then-sell transactions (you must sell before buying again) to maximise profit. The naive recurrence scans all earlier days and costs O(K·n²); keeping a running maximum of (previous-transaction profit − price) makes it O(K·n).
Worked example
prices = [2, 5, 7, 1, 4, 3, 1, 3], K = 3
State
dp[t][d] = best profit using at most t transactions up to and including day d.
Recurrence
dp[0][d] = 0, dp[t][0] = 0
dp[t][d] = max( dp[t][d-1], ← do nothing on day d
price[d] + max over m<d of ( dp[t-1][m] − price[m] ) )
The inner max is carried as a single running variable, so each cell is O(1).
DP table
The price series
day
0
1
2
3
4
5
6
7
price
2
5
7
1
4
3
1
3
dp[t][d] — each row allows one more transaction than the row above
txns \ day
0
1
2
3
4
5
6
7
≤ 0
0
0
0
0
0
0
0
0
≤ 1
0
3
5
5
5
5
5
5
≤ 2
0
3
5
5
8
8
8
8
≤ 3
0
3
5
5
8
8
8
10
Gold = the answer. Row 0 is all zeros: no transactions, no profit.
int maxProfit(const vector<int>& prices, int K) {
int n = prices.size();
if (n < 2) return 0;
vector<vector<int>> dp(K + 1, vector<int>(n, 0));
for (int t = 1; t <= K; ++t) {
int best = -prices[0]; // max(dp[t-1][m] - prices[m]) so far
for (int d = 1; d < n; ++d) {
dp[t][d] = max(dp[t][d-1], prices[d] + best);
best = max(best, dp[t-1][d] - prices[d]);
}
}
return dp[K][n-1];
}
Glob-style matching: '?' matches exactly one character, '*' matches any sequence including the empty one. Simpler than regex matching (#10) because '*' stands alone rather than modifying the character before it.
Worked example
text = "xbylmz", pattern = "x?y*z"
State
dp[i][j] = true if the first i pattern characters match the first j text characters.
Recurrence
dp[0][0] = true; dp[i][0] = dp[i-1][0] when p[i] == '*'
p[i] == '*': dp[i][j] = dp[i-1][j] OR dp[i][j-1]
match empty absorb one more character
p[i] == '?' or p[i] == t[j]: dp[i][j] = dp[i-1][j-1]
otherwise: dp[i][j] = false
DP table
dp[i][j] — the '*' row is the only one that can spread truth sideways
pat \ text
–
x
b
y
l
m
z
–
T
F
F
F
F
F
F
x
F
T
F
F
F
F
F
?
F
F
T
F
F
F
F
y
F
F
F
T
F
F
F
*
F
F
F
T
T
T
T
z
F
F
F
F
F
F
T
Green = matches, grey = does not. Gold = full pattern vs full text.
Break a paragraph into lines of at most L characters so that the total “badness” is minimised, where a line's badness is the square of its trailing whitespace. Squaring is what makes the algorithm spread slack evenly instead of dumping it all on one line — greedy line-filling does not.
Worked example
words = ["Tushar", "Roy", "likes", "to", "code"], line width L = 10
State
cost[i][j] = badness of putting words i..j on one line (∞ if they do not fit). dp[j] = minimum total badness for the first j words.
Recurrence
extra = L − (sum of word lengths i..j) − (j − i) ← the j−i single spaces
cost[i][j] = extra² if extra ≥ 0, else ∞
dp[0] = 0
dp[j] = min over i in [0, j-1] of dp[i] + cost[i][j-1]
DP table
cost[i][j] — badness of a line holding words i through j
from \ to
0:Tushar
1:Roy
2:likes
3:to
4:code
0:Tushar
16
0
∞
∞
∞
1:Roy
∞
49
1
∞
∞
2:likes
∞
∞
25
4
∞
3:to
∞
∞
∞
64
9
4:code
∞
∞
∞
∞
36
Blue = the three lines actually chosen. Grey ∞ = the words do not fit on one line.
TimeO(n²)SpaceO(n²) for the cost table, O(n) for dp
Implementation (C++)
int justify(const vector<string>& words, int L) {
int n = words.size();
const int INF = INT_MAX / 2;
vector<vector<int>> cost(n, vector<int>(n, INF));
for (int i = 0; i < n; ++i) {
int total = 0;
for (int j = i; j < n; ++j) {
total += words[j].size();
int extra = L - total - (j - i);
if (extra < 0) break;
cost[i][j] = extra * extra;
}
}
vector<int> dp(n + 1, INF);
dp[0] = 0;
for (int j = 1; j <= n; ++j)
for (int i = 0; i < j; ++i)
if (cost[i][j-1] < INF)
dp[j] = min(dp[j], dp[i] + cost[i][j-1]);
return dp[n];
}
Every cell of a grid has a cost. Starting top-left and moving only right or down, reach the bottom-right cell as cheaply as possible. The purest grid DP there is — each cell needs only its neighbour above and its neighbour to the left.
Worked example
the 3×4 cost grid below; moves allowed: right and down
State
dp[i][j] = cheapest total cost to reach cell (i, j) from (0, 0).
Recurrence
dp[0][0] = M[0][0]
dp[0][j] = dp[0][j-1] + M[0][j] ← top row: only from the left
dp[i][0] = dp[i-1][0] + M[i][0] ← left column: only from above
dp[i][j] = M[i][j] + min( dp[i-1][j], dp[i][j-1] )
DP table
The cost grid, with the cheapest path highlighted
row \ col
0
1
2
3
0
1
3
5
8
1
4
2
1
7
2
4
3
2
3
dp[i][j] — cumulative cheapest cost to reach each cell
row \ col
0
1
2
3
0
1
4
9
17
1
5
6
7
14
2
9
9
9
12
Gold = destination. Blue = the path recovered by walking back to the origin.
Coins of different values lie in a row. Two players alternate, each taking a coin from either end. Both play optimally; how much can the first player guarantee? The subtlety is that after your move the opponent chooses, so you must assume the worse of their two replies — hence a min nested inside the max.
Worked example
coins = [3, 9, 1, 2]
State
dp[i][j] = maximum the player to move can guarantee from the sub-row coins[i..j].
Recurrence
dp[i][i] = coins[i]
dp[i][j] = max(
coins[i] + min( dp[i+2][j], dp[i+1][j-1] ), ← you take the left coin
coins[j] + min( dp[i+1][j-1], dp[i][j-2] ) ) ← you take the right coin
The inner min is the opponent choosing the reply that is worst for you.
DP table
dp[i][j] over sub-rows — fills diagonally outward
i \ j
0:3
1:9
2:1
3:2
0:3
3
9
4
11
1:9
9
9
10
2:1
1
2
3:2
2
Gold = the whole row. Diagonal = a single coin, which the mover simply takes.
int optimalStrategy(const vector<int>& c) {
int n = c.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int i = 0; i < n; ++i) dp[i][i] = c[i];
for (int L = 2; L <= n; ++L)
for (int i = 0; i + L - 1 < n; ++i) {
int j = i + L - 1;
int a = (i + 2 <= j) ? dp[i+2][j] : 0;
int b = (i + 1 <= j - 1) ? dp[i+1][j-1] : 0;
int d = (i <= j - 2) ? dp[i][j-2] : 0;
dp[i][j] = max(c[i] + min(a, b), // take the left coin
c[j] + min(b, d)); // take the right coin
}
return dp[0][n-1];
}
Find the largest square (not rectangle) made entirely of 1s. A cell can be the bottom-right corner of a k×k square only if its top, left and top-left neighbours can each anchor a (k−1)×(k−1) square — so the recurrence is 1 + the minimum of three.
Worked example
the 6×5 binary matrix below
State
dp[i][j] = side length of the largest all-1s square whose bottom-right corner is (i, j).
Recurrence
dp[i][j] = 0 if M[i][j] == 0
dp[i][j] = 1 if M[i][j] == 1 and i == 0 or j == 0
dp[i][j] = 1 + min( dp[i-1][j], dp[i][j-1], dp[i-1][j-1] ) otherwise
answer = max cell in the table
DP table
The input matrix, with the winning square highlighted
row \ col
0
1
2
3
4
0
0
1
1
0
1
1
1
1
0
1
0
2
0
1
1
1
0
3
1
1
1
1
0
4
1
1
1
1
1
5
0
0
0
0
0
dp[i][j] — each cell is the side of the biggest square ending there
row \ col
0
1
2
3
4
0
0
1
1
0
1
1
1
1
0
1
0
2
0
1
1
1
0
3
1
1
2
2
0
4
1
2
2
3
1
5
0
0
0
0
0
Gold = the maximum cell (bottom-right corner). Blue = the square it represents.
Answer: max dp = 3 at dp[4][3] — a 3×3 square in rows 2–4, columns 1–3
TimeO(R·C)SpaceO(R·C), reducible to O(C)
Implementation (C++)
int maxSquare(const vector<vector<int>>& M) {
int R = M.size(), C = M[0].size(), best = 0;
vector<vector<int>> dp(R, vector<int>(C, 0));
for (int i = 0; i < R; ++i)
for (int j = 0; j < C; ++j)
if (M[i][j]) {
dp[i][j] = (i == 0 || j == 0)
? 1
: 1 + min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
best = max(best, dp[i][j]);
}
return best;
}
Bursting balloon i earns left×i×right, where left and right are its current neighbours — so the array shrinks and the neighbours keep changing. The trick is to invert the question: instead of asking which balloon to burst first, ask which one to burst LAST in each open interval. That balloon's neighbours are then fixed at the interval edges, and the two sides become independent subproblems.
Worked example
balloons = [3, 1, 5, 8], padded with virtual 1s at both ends
State
dp[i][j] = maximum coins from bursting every balloon strictly between i and j, with i and j themselves left intact.
Recurrence
dp[i][j] = 0 when j ≤ i + 1 (nothing strictly between)
dp[i][j] = max over k in (i, j) of
a[i]·a[k]·a[j] + dp[i][k] + dp[k][j]
k is the balloon burst LAST, which is why its neighbours are a[i] and a[j].
DP table
dp[i][j] over open intervals (i, j); indices include the two padding balloons
i \ j
L=1
0:3
1:1
2:5
3:8
R=1
L=1
0
0
3
30
159
167
0:3
0
0
15
135
159
1:1
0
0
40
48
2:5
0
0
40
3:8
0
0
R=1
0
Gold = the full padded range. Cells with j ≤ i+1 are empty or zero: no balloons inside.
Answer: dp[0][5] = 167 (burst 1, then 5, then 3, then 8)
TimeO(n³)SpaceO(n²)
Implementation (C++)
int maxCoins(const vector<int>& nums) {
vector<int> a;
a.push_back(1);
a.insert(a.end(), nums.begin(), nums.end());
a.push_back(1);
int n = a.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int L = 2; L < n; ++L) // interval width
for (int i = 0; i + L < n; ++i) {
int j = i + L;
for (int k = i + 1; k < j; ++k)
dp[i][j] = max(dp[i][j],
a[i]*a[k]*a[j] + dp[i][k] + dp[k][j]);
}
return dp[0][n-1];
}
Cut a string into pieces so that every piece is a palindrome, using as few cuts as possible. Done in two stages: first a boolean table of which substrings are palindromes, then a linear pass that finds the cheapest place to make the last cut.
Worked example
s = "banana"
State
pal[i][j] = is s[i..j] a palindrome? cuts[j] = fewest cuts needed for the prefix s[0..j].
Recurrence
pal[i][i] = true
pal[i][j] = ( s[i] == s[j] ) AND ( j − i < 2 OR pal[i+1][j-1] )
cuts[j] = 0 if pal[0][j]
cuts[j] = min over i in [1, j] with pal[i][j] of cuts[i-1] + 1
DP table
pal[i][j] — which substrings are palindromes (upper triangle only)
i \ j
0:b
1:a
2:n
3:a
4:n
5:a
0:b
T
F
F
F
F
F
1:a
T
F
T
F
T
2:n
T
F
T
F
3:a
T
F
T
4:n
T
F
5:a
T
Green = palindrome. Note pal[1][5] is true, which is what makes one cut enough.
cuts[j] — fewest cuts for each prefix
row \ index
0
1
2
3
4
5
s[j]
b
a
n
a
n
a
cuts[j]
0
1
2
1
2
1
Gold = the answer.
Answer: cuts[5] = 1 → "b" | "anana"
TimeO(n²)SpaceO(n²) for the palindrome table
Implementation (C++)
int minCut(const string& s) {
int n = s.size();
vector<vector<char>> pal(n, vector<char>(n, false));
for (int L = 1; L <= n; ++L)
for (int i = 0; i + L - 1 < n; ++i) {
int j = i + L - 1;
pal[i][j] = (s[i] == s[j]) && (L < 3 || pal[i+1][j-1]);
}
vector<int> cuts(n, 0);
for (int j = 0; j < n; ++j) {
if (pal[0][j]) { cuts[j] = 0; continue; }
cuts[j] = INT_MAX;
for (int i = 1; i <= j; ++i)
if (pal[i][j]) cuts[j] = min(cuts[j], cuts[i-1] + 1);
}
return cuts[n-1];
}
Stack 3D boxes to build the tallest tower, where a box may sit on another only if both base dimensions are strictly smaller. Boxes can be rotated and each may be used any number of times — but generating all three rotations up front and then forbidding repeats gives the same answer, since two identical bases can never stack.
Worked example
boxes (h, w, d) = (4,6,7), (1,2,3), (4,5,6), (10,12,32) → 12 rotations, sorted by base area
State
dp[i] = tallest tower whose top box is rotation i (after sorting by base area, descending).
Recurrence
Generate 3 rotations per box, normalise each base so width ≤ depth,
sort all rotations by base area descending.
dp[i] = height[i] + max{ dp[j] : j < i, width[j] > width[i], depth[j] > depth[i] }
answer = max over all i of dp[i]
This is Longest Increasing Subsequence with a 2D comparison and height as the weight.
DP table
All 12 rotations sorted by base area, with the running tower heights
base (w×d)
height
base area
dp[i]
prev[i]
0
12×32
10
384
10
–
1
10×32
12
320
12
–
2
10×12
32
120
42
0
3
6×7
4
42
46
2
4
5×6
4
30
50
3
5
4×7
6
28
48
2
6
4×6
7
24
53
3
7
4×6
5
24
51
3
8
4×5
6
20
56
4
9
2×3
1
6
57
8
10
1×3
2
3
58
8
11
1×2
3
2
60
9
Gold = the tallest tower. Blue = the boxes stacked in it, top of the list being lowest.
Answer: max dp = 60
TimeO(n²) after an O(n log n) sortSpaceO(n)
Implementation (C++)
struct Box { int h, w, d; };
int boxStacking(const vector<Box>& boxes) {
vector<Box> rot;
for (const Box& b : boxes) {
int t[3][3] = {{b.h, b.w, b.d}, {b.w, b.h, b.d}, {b.d, b.h, b.w}};
for (auto& r : t)
rot.push_back({r[0], min(r[1], r[2]), max(r[1], r[2])});
}
sort(rot.begin(), rot.end(), [](const Box& x, const Box& y) {
return x.w * x.d > y.w * y.d; // base area, descending
});
int n = rot.size(), best = 0;
vector<int> dp(n);
for (int i = 0; i < n; ++i) {
dp[i] = rot[i].h;
for (int j = 0; j < i; ++j)
if (rot[j].w > rot[i].w && rot[j].d > rot[i].d)
dp[i] = max(dp[i], dp[j] + rot[i].h);
best = max(best, dp[i]);
}
return best;
}
Decide whether string c is formed by interleaving a and b — that is, whether c can be split into pieces alternating from a and b while preserving the order within each. Greedy character matching fails when both a and b offer the same next character.
Worked example
a = "XXY", b = "XXZ", c = "XXZXXY"
State
dp[i][j] = true if the first i characters of a and the first j of b interleave to form the first i+j characters of c.
Recurrence
dp[0][0] = true
dp[i][j] = ( a[i] == c[i+j] AND dp[i-1][j] )
OR ( b[j] == c[i+j] AND dp[i][j-1] )
Position in c is always i+j — no third index is needed.
DP table
dp[i][j] — how many characters consumed from a (rows) and from b (columns)
a \ b
–
X
X
Z
–
T
T
T
T
X
T
T
F
T
X
T
F
F
T
Y
F
F
F
T
Green = this prefix pair works. Gold = both strings fully consumed.
Answer: dp[3][3] = true
TimeO(n·m)SpaceO(n·m), reducible to O(m)
Implementation (C++)
bool isInterleave(const string& a, const string& b, const string& c) {
int n = a.size(), m = b.size();
if (n + m != (int)c.size()) return false;
vector<vector<char>> dp(n + 1, vector<char>(m + 1, false));
dp[0][0] = true;
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= m; ++j) {
if (i && a[i-1] == c[i+j-1] && dp[i-1][j]) dp[i][j] = true;
if (j && b[j-1] == c[i+j-1] && dp[i][j-1]) dp[i][j] = true;
}
return dp[n][m];
}
Count the distinct ways to climb n steps taking 1 or 2 steps at a time. The answer is the Fibonacci sequence, and this is the smallest problem that shows the whole DP idea: the last move was either a 1-step or a 2-step, so the counts simply add.
Worked example
n = 10 steps; second row shows the same problem allowing steps of 1, 2 or 3
State
dp[i] = number of distinct ways to reach step i.
Recurrence
dp[0] = 1 (one way to stand at the bottom: do nothing)
dp[i] = dp[i-1] + dp[i-2] ← steps of 1 or 2
dp[i] = dp[i-1] + dp[i-2] + dp[i-3] ← steps of 1, 2 or 3
DP table
Both variants side by side
steps \ n
0
1
2
3
4
5
6
7
8
9
10
{1, 2}
1
1
2
3
5
8
13
21
34
55
89
{1, 2, 3}
1
1
2
4
7
13
24
44
81
149
274
Gold = the answer for n = 10. Row 1 is Fibonacci; row 2 is the tribonacci sequence.
Answer: dp[10] = 89 with steps {1,2}; 274 with steps {1,2,3}
TimeO(n·k) for k allowed step sizesSpaceO(k) — only the last k values matter
Implementation (C++)
long long climb(int n, const vector<int>& steps = {1, 2}) {
vector<long long> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; ++i)
for (int s : steps)
if (i - s >= 0) dp[i] += dp[i - s];
return dp[n];
}
Pick a subset of array elements with the largest possible sum, subject to never picking two neighbours. Known elsewhere as the House Robber problem. At every index the choice is binary — skip it and inherit, or take it and jump back two.
Worked example
a = [1, 2, 9, 4, 5, 0, 4, 11, 6]
State
dp[i] = best non-adjacent sum considering only a[0..i].
Gold = the answer. Blue = the elements selected on the traceback.
Answer: dp[8] = 26 (indices 0, 2, 4, 7 → 1 + 9 + 5 + 11, no two adjacent)
TimeO(n)SpaceO(1) — two rolling variables suffice
Implementation (C++)
int maxNonAdjacent(const vector<int>& a) {
int incl = 0, excl = 0; // best including / excluding the previous element
for (int x : a) {
int nextIncl = excl + x;
excl = max(incl, excl);
incl = nextIncl;
}
return max(incl, excl);
}
Given n distinct sorted keys, how many structurally different binary SEARCH trees can be built? Because the keys are sorted, choosing key r as the root forces the first r keys into the left subtree and the rest into the right — so only the counts matter, not the key values. The result is the Catalan numbers.
Worked example
n = 5 keys
State
dp[i] = number of distinct BSTs on i keys.
Recurrence
dp[0] = 1 (the empty tree)
dp[i] = Σ over r in [0, i-1] of dp[r] · dp[i-1-r]
r keys go left, i-1-r go right, one key is the root
Closed form: dp[n] = C(2n, n) / (n+1), the nth Catalan number.
DP table
dp[i] with the convolution that produced each value
row \ n
0
1
2
3
4
5
6
dp[n]
1
1
2
5
14
42
132
sum of left·right
1
1·1
1·1 + 1·1
1·2 + 1·1 + 2·1
1·5 + 1·2 + 2·1 + 5·1
1·14 + 1·5 + 2·2 + 5·1 + 14·1
1·42 + 1·14 + 2·5 + 5·2 + 14·1 + 42·1
Gold = the answer for n = 5. The sequence 1, 1, 2, 5, 14, 42, 132 is Catalan.
Answer: dp[5] = 42
TimeO(n²)SpaceO(n)
Implementation (C++)
long long countBST(int n) {
vector<long long> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; ++i)
for (int r = 0; r < i; ++r)
dp[i] += dp[r] * dp[i - 1 - r];
return dp[n];
}
Find the increasing subsequence with the largest SUM rather than the greatest length. Same scan as LIS (#06), but each cell accumulates values instead of counting elements — and the two answers often disagree, as they do here.
Worked example
a = [1, 101, 2, 3, 100, 4, 5]
State
dp[i] = largest sum of an increasing subsequence ending exactly at index i.
Recurrence
dp[i] = a[i] initially
dp[i] = a[i] + max{ dp[j] : j < i and a[j] < a[i] }
answer = max over all i of dp[i]
DP table
dp[i] scanned left to right; prev records which index each cell extended
row \ index
0
1
2
3
4
5
6
a[i]
1
101
2
3
100
4
5
dp[i]
1
102
3
6
106
10
15
prev[i]
–
0
0
2
3
3
5
Gold = the maximum. Blue = the subsequence achieving it.
Answer: max dp = 106 (1 + 2 + 3 + 100) — note the longest subsequence here is 1,2,3,4,5 summing to only 15
TimeO(n²)SpaceO(n)
Implementation (C++)
int maxSumIncreasing(const vector<int>& a) {
int n = a.size(), best = 0;
vector<int> dp(a); // dp[i] starts at a[i]
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j)
if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + a[i]);
best = max(best, dp[i]);
}
return best;
}
Count the distinct paths from the top-left cell to the bottom-right cell of a grid when only right and down moves are allowed. Every cell is reached from exactly two places, so the counts simply add — the table is Pascal's triangle laid on its side.
Worked example
a 3×4 grid
State
dp[i][j] = number of distinct paths from (0, 0) to (i, j).
Recurrence
dp[0][j] = dp[i][0] = 1 (one straight path along each edge)
dp[i][j] = dp[i-1][j] + dp[i][j-1]
Closed form: C(R+C-2, R-1).
DP table
dp[i][j] — path counts; the first row and column are all 1s
row \ col
0
1
2
3
0
1
1
1
1
1
1
2
3
4
2
1
3
6
10
Gold = the destination.
Answer: dp[2][3] = 10 = C(5, 2)
TimeO(R·C)SpaceO(C)
Implementation (C++)
long long countPaths(int R, int C) {
vector<vector<long long>> dp(R, vector<long long>(C, 1));
for (int i = 1; i < R; ++i)
for (int j = 1; j < C; ++j)
dp[i][j] = dp[i-1][j] + dp[i][j-1];
return dp[R-1][C-1];
}
The identical problem to #01, solved by writing the recursion directly and caching results. The point of this table is what is MISSING: bottom-up fills all 40 cells, while memoisation only ever computes the 10 states the recursion actually reaches. When the reachable state space is sparse, top-down wins; when it is dense, bottom-up wins on constant factors and needs no recursion stack.
Worked example
weights = [1, 3, 4, 5], values = [1, 4, 5, 7], capacity W = 7 (same instance as #01)
State
solve(i, c) = best value from the first i items with capacity c, cached in a 2D table indexed by (i, c).
Recurrence
solve(0, c) = solve(i, 0) = 0
solve(i, c) = solve(i-1, c) if w[i] > c
solve(i, c) = max( solve(i-1, c), v[i] + solve(i-1, c-w[i]) ) otherwise
Identical maths to #01 — only the evaluation order differs.
DP table
The memo table — grey dots are states the recursion never visited
item \ cap
0
1
2
3
4
5
6
7
– (base)
·
·
·
·
·
·
·
·
w=1, v=1
·
·
1
1
1
·
·
1
w=3, v=4
·
·
1
4
·
·
·
5
w=4, v=5
·
·
1
·
·
·
·
9
w=5, v=7
·
·
·
·
·
·
·
9
Gold = the top-level call. Blue = states actually computed. Grey · = never touched.
Answer: solve(4, 7) = 9, computed from 10 of the 40 possible states
TimeO(n·W) worst case, often far lessSpaceO(n·W) cache + O(n) recursion stack
Implementation (C++)
int knapsackTopDown(const vector<int>& w, const vector<int>& v, int W) {
int n = w.size();
vector<vector<int>> memo(n + 1, vector<int>(W + 1, -1));
function<int(int, int)> solve = [&](int i, int c) -> int {
if (i == 0 || c == 0) return 0;
int& cached = memo[i][c];
if (cached != -1) return cached;
int best = solve(i - 1, c);
if (w[i-1] <= c)
best = max(best, v[i-1] + solve(i - 1, c - w[i-1]));
return cached = best;
};
return solve(n, W);
}
Find the longest subsequence that first increases and then decreases. Rather than a new recurrence, run LIS twice — once forward, once backward — and for each index add the two lengths, subtracting 1 because the peak element is counted in both.
Worked example
a = [1, 11, 2, 10, 4, 5, 2, 1]
State
inc[i] = longest increasing subsequence ending at i. dec[i] = longest decreasing subsequence starting at i.
Recurrence
inc[i] = 1 + max{ inc[j] : j < i, a[j] < a[i] } ← left-to-right pass
dec[i] = 1 + max{ dec[j] : j > i, a[j] < a[i] } ← right-to-left pass
answer = max over i of inc[i] + dec[i] − 1
Index i is the peak; subtract 1 so it is not double counted.
DP table
Two LIS passes and their combination
row \ index
0
1
2
3
4
5
6
7
a[i]
1
11
2
10
4
5
2
1
inc[i] →
1
2
2
3
3
4
2
1
dec[i] ←
1
5
2
4
3
3
2
1
inc+dec−1
1
6
3
6
5
6
3
1
Gold = the best peak. Read the last row as “longest bitonic subsequence peaking here”.
Answer: max = 6 at index 3 (the subsequence 1, 2, 10, 4, 2, 1)
TimeO(n²)SpaceO(n)
Implementation (C++)
int longestBitonic(const vector<int>& a) {
int n = a.size();
vector<int> inc(n, 1), dec(n, 1);
for (int i = 0; i < n; ++i) // left-to-right LIS
for (int j = 0; j < i; ++j)
if (a[j] < a[i]) inc[i] = max(inc[i], inc[j] + 1);
for (int i = n - 1; i >= 0; --i) // right-to-left LIS
for (int j = i + 1; j < n; ++j)
if (a[j] < a[i]) dec[i] = max(dec[i], dec[j] + 1);
int best = 0;
for (int i = 0; i < n; ++i) best = max(best, inc[i] + dec[i] - 1);
return best;
}
Count binary strings of length n that never contain two adjacent 1s. The move that makes it easy is splitting the count by what the string ENDS with: a string ending in 0 can be extended either way, but a string ending in 1 can only be extended with a 0.
Worked example
n = 5 bits
State
zero[i] = count of valid strings of length i ending in 0. ones[i] = ending in 1.
Recurrence
zero[1] = 1, ones[1] = 1
zero[i] = zero[i-1] + ones[i-1] ← appending 0 is always safe
ones[i] = zero[i-1] ← appending 1 needs a 0 before it
answer = zero[n] + ones[n] = Fibonacci(n+2)
DP table
The two states and their total
row \ length
0
1
2
3
4
5
6
ending in 0
–
1
2
3
5
8
13
ending in 1
–
1
1
2
3
5
8
total
–
2
3
5
8
13
21
Gold = the answer for n = 5. Each row is a shifted Fibonacci sequence.
Answer: zero[5] + ones[5] = 8 + 5 = 13
TimeO(n)SpaceO(1)
Implementation (C++)
long long countNoConsecutiveOnes(int n) {
long long zero = 1, ones = 1; // length-1 strings: "0" and "1"
for (int i = 2; i <= n; ++i) {
long long z = zero + ones; // appending 0 is always safe
long long o = zero; // appending 1 needs a 0 before it
zero = z;
ones = o;
}
return zero + ones;
}
Answer many “sum of this rectangle” queries on a matrix that never changes. Precompute a prefix-sum table once in O(R·C), then every query is four lookups — the two overlapping strips get subtracted, so the corner they share is added back. Inclusion–exclusion in two dimensions.
Worked example
the 4×4 matrix below; query = sum of rows 1–2, columns 1–2
State
S[i][j] = sum of the rectangle from (0,0) to (i-1, j-1). The extra zero row and column remove every boundary special case.
The minimum-coin problem again, written as plain recursion with a cache. Compare with #05 and #20: the recurrence is unchanged, but evaluation starts at the target amount and descends. Here every amount happens to be reachable, so the memo fills completely — with large denominations and a large target, most of it would stay empty.
Worked example
coins = [1, 5, 6, 9], amount A = 11 (same instance as #05)
State
solve(a) = fewest coins to make amount a, cached in an array indexed by amount.
Recurrence
solve(0) = 0
solve(a) = 1 + min{ solve(a − c) : c in coins, c ≤ a }
Called top-down from a = A; each distinct amount is solved once.
DP table
The memo table after the call — grey dots would mark unvisited amounts
row \ amount
0
1
2
3
4
5
6
7
8
9
10
11
amount
0
1
2
3
4
5
6
7
8
9
10
11
solve(a)
0
1
2
3
4
1
1
2
3
1
2
2
Gold = the top-level call. Blue = amounts computed and cached.
Answer: solve(11) = 2 (5 + 6)
TimeO(n·A)SpaceO(A) cache + O(A) recursion depth in the worst case
Implementation (C++)
int minCoinsTopDown(const vector<int>& coins, int A) {
const int INF = INT_MAX / 2;
vector<int> memo(A + 1, -1);
function<int(int)> solve = [&](int a) -> int {
if (a == 0) return 0;
int& cached = memo[a];
if (cached != -1) return cached;
int best = INF;
for (int c : coins)
if (c <= a) best = min(best, solve(a - c) + 1);
return cached = best;
};
int r = solve(A);
return r >= INF ? -1 : r;
}
In a grid of X and O, find the largest square whose BORDER is entirely X — the inside may be anything. Two precomputed tables make each candidate check O(1): how far X extends to the right of a cell, and how far it extends downward.
Worked example
the 5×6 grid of X and O below
State
hor[i][j] = consecutive Xs starting at (i, j) going right. ver[i][j] = consecutive Xs starting at (i, j) going down.
Recurrence
hor[i][j] = 0 if grid[i][j] == 'O', else 1 + hor[i][j+1]
ver[i][j] = 0 if grid[i][j] == 'O', else 1 + ver[i+1][j]
For each top-left corner (i, j), try side k from min(hor, ver) downward:
valid if hor[i+k-1][j] ≥ k (bottom edge)
and ver[i][j+k-1] ≥ k (right edge)
The top and left edges are already guaranteed by hor[i][j] and ver[i][j].
DP table
The grid, with the winning square's border highlighted
row \ col
0
1
2
3
4
5
0
X
O
X
X
X
X
1
X
O
X
O
O
X
2
X
X
X
O
O
X
3
X
X
X
X
X
X
4
X
O
X
O
X
O
hor[i][j] — Xs continuing to the right
row \ col
0
1
2
3
4
5
0
1
0
4
3
2
1
1
1
0
1
0
0
1
2
3
2
1
0
0
1
3
6
5
4
3
2
1
4
1
0
1
0
1
0
Blue = the winning top-left corner.
ver[i][j] — Xs continuing downward
row \ col
0
1
2
3
4
5
0
5
0
5
1
1
4
1
4
0
4
0
0
3
2
3
2
3
0
0
2
3
2
1
2
1
2
1
4
1
0
1
0
1
0
Blue = the same corner. Both must be ≥ the side length.
Answer: side 4 — top-left corner at (0, 2), border highlighted above
TimeO(R·C·min(R,C)) worst caseSpaceO(R·C)
Implementation (C++)
int maxXSquare(const vector<string>& g) {
int R = g.size(), C = g[0].size();
vector<vector<int>> hor(R, vector<int>(C, 0)), ver(R, vector<int>(C, 0));
for (int i = R - 1; i >= 0; --i)
for (int j = C - 1; j >= 0; --j)
if (g[i][j] == 'X') {
hor[i][j] = 1 + (j + 1 < C ? hor[i][j+1] : 0);
ver[i][j] = 1 + (i + 1 < R ? ver[i+1][j] : 0);
}
int best = 0;
for (int i = 0; i < R; ++i)
for (int j = 0; j < C; ++j)
for (int k = min(hor[i][j], ver[i][j]); k > best; --k)
if (hor[i+k-1][j] >= k && ver[i][j+k-1] >= k) {
best = k; // bottom and right edges hold
break;
}
return best;
}
Given only the LENGTH of a preorder traversal — not the values — count how many distinct binary trees could have produced it. In preorder the first element is the root and the remaining n−1 elements split into a left block and a right block, so the count depends only on the sizes. Same Catalan recurrence as #33, arrived at from a different direction.
Worked example
preorder length n = 5
State
dp[i] = number of distinct binary trees with i nodes.
Recurrence
dp[0] = 1 (the empty tree)
dp[i] = Σ over r in [0, i-1] of dp[r] · dp[i-1-r]
r nodes to the left of the root, i-1-r to the right
Identical to #33 — for unlabelled shapes, “how many BSTs on sorted keys” and
“how many binary trees on n nodes” are the same question.
DP table
dp[i] with the left×right convolution behind each value
row \ n
0
1
2
3
4
5
6
dp[n]
1
1
2
5
14
42
132
sum of left·right
1
1·1
1·1 + 1·1
1·2 + 1·1 + 2·1
1·5 + 1·2 + 2·1 + 5·1
1·14 + 1·5 + 2·2 + 5·1 + 14·1
1·42 + 1·14 + 2·5 + 5·2 + 14·1 + 42·1
Gold = the answer for n = 5. Compare with problem #33 — identical numbers.
Answer: dp[5] = 42
TimeO(n²)SpaceO(n)
Implementation (C++)
long long countTrees(int n) {
vector<long long> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; ++i)
for (int r = 0; r < i; ++r)
dp[i] += dp[r] * dp[i - 1 - r];
return dp[n];
}