Dynamic Programming Short Notes
Dynamic Programming Short Notes
Use it for quick revision before interviews: read the 'Recognize It' and 'Recurrence Relation' boxes for every problem
in one pass, then re-derive the code from memory and check it against the printed solution.
Chapter 0 — Foundations of Dynamic Programming
Before solving any DP problem, understand these four pillars: recursion (brute force), overlapping subproblems,
optimal substructure, and state. Every problem in this playlist is solved using the same five-step method taught by
Aditya Verma: (1) write the recursive brute force, (2) identify what changes between calls — that becomes your state,
(3) memoize with an array/map keyed on that state, (4) convert to a bottom-up table, (5) optimize space if only a few
previous rows/states are needed.
How to Recognize It
• Overlapping subproblems: the same recursive call is made with identical parameters more than once.
• Optimal substructure: the optimal answer to the problem can be built from optimal answers to its subproblems.
• State: the minimal set of changing parameters that uniquely identifies a subproblem.
Recurrence Relation
No single formula — but every DP problem reduces to: solve(state) = combine( solve(smaller
state 1), solve(smaller state 2), ... )
How to Recognize It
• Recursion: exponential time, no extra memory beyond the call stack — correct but slow.
• Memoization (top-down): add a lookup table; before computing, check if state was already solved.
• Tabulation (bottom-up): remove recursion entirely, fill the table iteratively from base cases upward.
• Space optimization: if row i only depends on row i-1 (or a constant number of previous states), keep only those
rows.
C++17 Solution
// Generic template pattern used throughout this handbook
// 1) Recursion
int solveRec(/* state */) {
// base case
// choices -> recursive calls -> combine
}
// 2) Memoization
vector<vector<int>> dp(N+1, vector<int>(M+1, -1));
int solveMemo(/* state */) {
// base case
if (dp[i][j] != -1) return dp[i][j];
return dp[i][j] = /* recursive relation */;
}
// 3) Tabulation
vector<vector<int>> dp(N+1, vector<int>(M+1, 0));
// initialize base cases in dp[][]
for (int i = 1; i <= N; i++)
for (int j = 1; j <= M; j++)
dp[i][j] = /* same relation, using dp[i-1][..] etc. */;
// 4) Space optimization
vector<int> prev(M+1, 0), curr(M+1, 0);
// loop as above, swap(prev, curr) at the end of each outer iteration
How to Recognize It
• State: (index i, remaining capacity j) — these are the only two things that change between recursive calls.
• Choice at each item: include it (if it fits) or exclude it.
• Overlapping subproblems: many different item orders lead to the same (i, remaining capacity) pair.
Recurrence Relation
knapsack(i, j) = max( values[i] + knapsack(i-1, j-weights[i]) [if weights[i] <= j],
knapsack(i-1, j) )
Base case: knapsack(0, j) = 0 or knapsack(i, 0) = 0
C++17 Solution
int knapsack(vector<int>& wt, vector<int>& val, int n, int W) {
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; i++) {
for (int cap = 1; cap <= W; cap++) {
int exclude = dp[i - 1][cap];
int include = (wt[i - 1] <= cap)
? val[i - 1] + dp[i - 1][cap - wt[i - 1]]
: 0;
dp[i][cap] = max(include, exclude);
}
}
return dp[n][W];
}
Complexity: Recursion: O(2^n). Memoization/Tabulation: O(n*W) time, O(n*W) space. Space-optimized (1-D array,
iterate capacity right-to-left): O(W) space.
How to Recognize It
• Identical to 0/1 Knapsack with 'value' removed — we only track whether a sum is reachable, not an optimal
value.
• State: (index i, remaining sum j).
Recurrence Relation
isSubsetSum(i, j) = isSubsetSum(i-1, j) OR (arr[i] <= j AND isSubsetSum(i-1, j-arr[i]))
Base case: isSubsetSum(i, 0) = true; isSubsetSum(0, j>0) = false
C++17 Solution
bool subsetSum(vector<int>& arr, int n, int target) {
vector<vector<bool>> dp(n + 1, vector<bool>(target + 1, false));
for (int i = 0; i <= n; i++) dp[i][0] = true; // sum 0 always achievable
for (int i = 1; i <= n; i++) {
for (int s = 1; s <= target; s++) {
dp[i][s] = dp[i - 1][s];
if (arr[i - 1] <= s) dp[i][s] = dp[i][s] || dp[i - 1][s - arr[i - 1]];
}
}
return dp[n][target];
}
Complexity: O(n*target) time and space; O(target) with 1-D space optimization.
Recurrence Relation
Same as Subset Sum, with target = sum(arr) / 2.
C++17 Solution
bool canPartition(vector<int>& nums) {
int total = accumulate([Link](), [Link](), 0);
if (total % 2 != 0) return false;
return subsetSum(nums, [Link](), total / 2);
}
How to Recognize It
• Same state as Subset Sum, but the combine step adds counts instead of OR-ing booleans.
Recurrence Relation
countSS(i, j) = countSS(i-1, j) + (arr[i]<=j ? countSS(i-1, j-arr[i]) : 0)
Base case: countSS(i, 0) = 1 for all i (empty subset). If arr contains zeros, countSS(0,0)
needs special handling (multiply by 2 per zero).
C++17 Solution
int countSubsetSum(vector<int>& arr, int n, int target) {
vector<vector<int>> dp(n + 1, vector<int>(target + 1, 0));
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++)
for (int s = 0; s <= target; s++) {
dp[i][s] = dp[i - 1][s];
if (arr[i - 1] <= s) dp[i][s] += dp[i - 1][s - arr[i - 1]];
}
return dp[n][target];
}
Complexity: O(n*target) time and space.
How to Recognize It
• Run Subset Sum DP once for target = totalSum, keeping the full boolean row for i = n.
• Every reachable sum s in [0, totalSum/2] gives a valid partition value; the answer is min(totalSum - 2*s) over
reachable s.
Recurrence Relation
Reuses Subset Sum table; answer = min over s in [0, sum/2] where dp[n][s] is true of (sum
- 2*s).
C++17 Solution
int minSubsetSumDiff(vector<int>& arr, int n) {
int total = accumulate([Link](), [Link](), 0);
vector<vector<bool>> dp(n + 1, vector<bool>(total + 1, false));
for (int i = 0; i <= n; i++) dp[i][0] = true;
for (int i = 1; i <= n; i++)
for (int s = 0; s <= total; s++) {
dp[i][s] = dp[i - 1][s];
if (arr[i - 1] <= s) dp[i][s] = dp[i][s] || dp[i - 1][s - arr[i - 1]];
}
int best = INT_MAX;
for (int s = 0; s <= total / 2; s++)
if (dp[n][s]) best = min(best, total - 2 * s);
return best;
}
How to Recognize It
• Algebra: S1 + S2 = totalSum and S1 - S2 = diff => S1 = (totalSum + diff) / 2.
• Reduces exactly to Count of Subsets with a Given Sum, target = (totalSum + diff)/2.
Recurrence Relation
target = (total + diff) / 2; answer = countSubsetSum(arr, n, target)
C++17 Solution
int countSubsetsGivenDiff(vector<int>& arr, int n, int diff) {
int total = accumulate([Link](), [Link](), 0);
if ((total + diff) % 2 != 0 || total < diff) return 0; // no valid partition
return countSubsetSum(arr, n, (total + diff) / 2);
}
How to Recognize It
• Identical to Count of Subsets with Given Difference: positive-signed numbers form S1, negative-signed form S2,
and S1 - S2 = target.
Recurrence Relation
Same as 1.6 with diff = target.
C++17 Solution
int findTargetSumWays(vector<int>& nums, int target) {
int n = [Link]();
return countSubsetsGivenDiff(nums, n, target);
}
Complexity: O(n*total) time and space.
How to Recognize It
• State is still (i, capacity), but the 'include' branch stays on the same row i (not i-1) because the item can be
reused.
Recurrence Relation
dp(i, j) = max( val[i] + dp(i, j-wt[i]) [if wt[i]<=j], dp(i-1, j) )
C++17 Solution
int unboundedKnapsack(vector<int>& wt, vector<int>& val, int n, int W) {
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; i++)
for (int cap = 1; cap <= W; cap++) {
int exclude = dp[i - 1][cap];
int include = (wt[i - 1] <= cap)
? val[i - 1] + dp[i][cap - wt[i - 1]] // stay on row i
: 0;
dp[i][cap] = max(include, exclude);
}
return dp[n][W];
}
Complexity: O(n*W) time, O(n*W) space; O(W) space optimized (single 1-D row, iterate capacity left-to-right).
Recurrence Relation
length[i] = i+1 (1-indexed lengths); reuse Unbounded Knapsack with wt=length, val=price,
W=n.
C++17 Solution
int rodCutting(vector<int>& price, int n) {
vector<int> length(n);
for (int i = 0; i < n; i++) length[i] = i + 1;
return unboundedKnapsack(length, price, n, n);
}
How to Recognize It
• Unbounded Knapsack 'combine' step becomes addition of ways instead of max of value.
Recurrence Relation
dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]] [if coins[i-1] <= j]
Base case: dp[i][0] = 1 for all i.
C++17 Solution
int coinChangeWays(vector<int>& coins, int amount) {
int n = [Link]();
vector<vector<long long>> dp(n + 1, vector<long long>(amount + 1, 0));
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++)
for (int a = 1; a <= amount; a++) {
dp[i][a] = dp[i - 1][a];
if (coins[i - 1] <= a) dp[i][a] += dp[i][a - coins[i - 1]];
}
return (int)dp[n][amount];
}
How to Recognize It
• Same state, but combine step takes the minimum count and adds 1 coin when included.
Recurrence Relation
dp[i][j] = min( dp[i-1][j], 1 + dp[i][j-coins[i-1]] )
Base case: dp[i][0] = 0; dp[0][j>0] = INF (unreachable with zero coin types).
C++17 Solution
int coinChangeMin(vector<int>& coins, int amount) {
int n = [Link]();
const int INF = 1e9;
vector<vector<int>> dp(n + 1, vector<int>(amount + 1, INF));
for (int i = 0; i <= n; i++) dp[i][0] = 0;
for (int i = 1; i <= n; i++)
for (int a = 1; a <= amount; a++) {
dp[i][a] = dp[i - 1][a];
if (coins[i - 1] <= a)
dp[i][a] = min(dp[i][a], 1 + dp[i][a - coins[i - 1]]);
}
return dp[n][amount] >= INF ? -1 : dp[n][amount];
}
How to Recognize It
• State: (i, j) = lengths of prefixes of s1 and s2 still being considered.
• Optimal substructure: LCS of full strings depends only on LCS of shorter prefixes.
Recurrence Relation
lcs(i,j) = 1 + lcs(i-1,j-1) if s1[i-1]==s2[j-1]
lcs(i,j) = max(lcs(i-1,j), lcs(i,j-1)) otherwise
Base case: lcs(0,j)=lcs(i,0)=0
C++17 Solution
int lcs(string& s1, string& s2) {
int n = [Link](), m = [Link]();
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++)
dp[i][j] = (s1[i - 1] == s2[j - 1])
? 1 + dp[i - 1][j - 1]
: max(dp[i - 1][j], dp[i][j - 1]);
return dp[n][m];
}
How to Recognize It
• Same (i,j) state as LCS, but a mismatch resets the running length to 0 instead of taking a max.
Recurrence Relation
dp(i,j) = 1 + dp(i-1,j-1) if s1[i-1]==s2[j-1]
dp(i,j) = 0 otherwise
Answer = max value anywhere in the dp table (not necessarily dp[n][m]).
C++17 Solution
int longestCommonSubstring(string& s1, string& s2) {
int n = [Link](), m = [Link](), 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 (s1[i - 1] == s2[j - 1]) {
dp[i][j] = 1 + dp[i - 1][j - 1];
best = max(best, dp[i][j]);
}
// else dp[i][j] stays 0 (contiguity breaks)
}
return best;
}
How to Recognize It
• Build the standard LCS dp table first, then walk backwards from dp[n][m] following the same decisions that built
the table.
Recurrence Relation
Backtrack from (n, m): if s1[i-1]==s2[j-1], append char, move to (i-1,j-1); else move
toward whichever of dp[i-1][j], dp[i][j-1] is larger.
C++17 Solution
string printLCS(string& s1, string& s2) {
int n = [Link](), m = [Link]();
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++)
dp[i][j] = (s1[i-1]==s2[j-1]) ? 1+dp[i-1][j-1] : max(dp[i-1][j], dp[i][j-1]);
string result;
int i = n, j = m;
while (i > 0 && j > 0) {
if (s1[i - 1] == s2[j - 1]) { result += s1[i - 1]; i--; j--; }
else if (dp[i - 1][j] > dp[i][j - 1]) i--;
else j--;
}
reverse([Link](), [Link]());
return result;
}
Complexity: O(n*m) time and space (table must be fully materialized — space cannot be optimized away here).
How to Recognize It
• SCS length = n + m − LCS(s1, s2): keep every character of both strings, but do not duplicate the shared
subsequence.
Recurrence Relation
scsLength = n + m - lcs(s1, s2)
C++17 Solution
int shortestCommonSupersequenceLength(string& s1, string& s2) {
int n = [Link](), m = [Link]();
return n + m - lcs(s1, s2);
}
How to Recognize It
• Backtrack through the LCS table like 3.3, but when characters differ, emit the character belonging to whichever
pointer we move, and when they match, emit it once.
Recurrence Relation
Same backtracking walk as Printing LCS, but append every visited character (matched or
not) instead of skipping mismatches.
C++17 Solution
string printSCS(string& s1, string& s2) {
int n = [Link](), m = [Link]();
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++)
dp[i][j] = (s1[i-1]==s2[j-1]) ? 1+dp[i-1][j-1] : max(dp[i-1][j], dp[i][j-1]);
string result;
int i = n, j = m;
while (i > 0 && j > 0) {
if (s1[i - 1] == s2[j - 1]) { result += s1[i - 1]; i--; j--; }
else if (dp[i - 1][j] > dp[i][j - 1]) { result += s1[i - 1]; i--; }
else { result += s2[j - 1]; j--; }
}
while (i > 0) { result += s1[i - 1]; i--; }
while (j > 0) { result += s2[j - 1]; j--; }
reverse([Link](), [Link]());
return result;
}
How to Recognize It
• Keep the LCS (the common part) untouched; delete everything else from s1 and insert everything else from s2.
Recurrence Relation
deletions = n - lcs(s1,s2)
insertions = m - lcs(s1,s2)
C++17 Solution
pair<int,int> minInsertDelete(string& s1, string& s2) {
int n = [Link](), m = [Link]();
int l = lcs(s1, s2);
int deletions = n - l;
int insertions = m - l;
return {deletions, insertions};
}
How to Recognize It
• A palindromic subsequence of s is exactly the LCS of s and reverse(s).
Recurrence Relation
lps(s) = lcs(s, reverse(s))
C++17 Solution
int longestPalindromicSubsequence(string& s) {
string rev = s;
reverse([Link](), [Link]());
return lcs(s, rev);
}
How to Recognize It
• Whatever isn't part of the Longest Palindromic Subsequence must be deleted.
Recurrence Relation
minDeletions = n - lps(s)
C++17 Solution
int minDeletionsToPalindrome(string& s) {
return [Link]() - longestPalindromicSubsequence(s);
}
How to Recognize It
• Symmetric to 3.8: characters not part of the LPS need a mirrored insertion.
Recurrence Relation
minInsertions = n - lps(s)
C++17 Solution
int minInsertionsToPalindrome(string& s) {
return [Link]() - longestPalindromicSubsequence(s);
}
How to Recognize It
• Run LCS of s against itself, but disallow matching a character with itself at the same index (i != j).
Recurrence Relation
lrs(i,j) = 1 + lrs(i-1,j-1) if s[i-1]==s[j-1] AND i != j
lrs(i,j) = max(lrs(i-1,j), lrs(i,j-1)) otherwise
C++17 Solution
int longestRepeatingSubsequence(string& s) {
int n = [Link]();
vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
if (s[i - 1] == s[j - 1] && i != j)
dp[i][j] = 1 + dp[i - 1][j - 1];
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
return dp[n][n];
}
How to Recognize It
• s is a subsequence of t exactly when lcs(s, t) == [Link]().
Recurrence Relation
isSubsequence = (lcs(s, t) == [Link]())
C++17 Solution
bool isSubsequence(string s, string t) {
return lcs(s, t) == (int)[Link]();
}
Complexity: O(n*m) via LCS reduction; a linear two-pointer O(n+m) approach exists and is preferred for this exact
LeetCode problem in production code.
How to Recognize It
• Extend the state to (i, j, k) for a third string, or reuse two-string LCS directly depending on exact constraints
given.
Recurrence Relation
lcs3(i,j,k) = 1+lcs3(i-1,j-1,k-1) if all three chars match, else max over dropping one
index at a time.
C++17 Solution
int lcsOfThree(string& a, string& b, string& c) {
int n=[Link](), m=[Link](), p=[Link]();
vector<vector<vector<int>>> dp(n+1, vector<vector<int>>(m+1, vector<int>(p+1, 0)));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int k = 1; k <= p; k++) {
if (a[i-1]==b[j-1] && b[j-1]==c[k-1])
dp[i][j][k] = 1 + dp[i-1][j-1][k-1];
else
dp[i][j][k] = max({dp[i-1][j][k], dp[i][j-1][k], dp[i][j][k-1]});
}
return dp[n][m][p];
}
How to Recognize It
• State: (i, j) representing the subrange of matrices being multiplied.
• Try every split point k between i and j; cost = cost(i,k) + cost(k+1,j) + cost of multiplying the two resulting
matrices.
Recurrence Relation
mcm(i,j) = min over k in [i, j-1] of mcm(i,k) + mcm(k+1,j) + arr[i-1]*arr[k]*arr[j]
Base case: mcm(i,i) = 0 (a single matrix needs no multiplication).
C++17 Solution
int mcmRec(vector<int>& arr, int i, int j) {
if (i >= j) return 0; // base case: single matrix
int minCost = INT_MAX;
for (int k = i; k < j; k++) {
int cost = mcmRec(arr, i, k) + mcmRec(arr, k + 1, j)
+ arr[i - 1] * arr[k] * arr[j];
minCost = min(minCost, cost);
}
return minCost;
}
// dp[i][j] memo table follows the same recurrence with a 2D cache keyed on (i, j)
Complexity: Recursion: exponential (Catalan-number growth). Memoization/Tabulation: O(n^3) time, O(n^2) space.
How to Recognize It
• Same interval (i, j) state with a split point k; the 'cost' formula changes per problem but the loop skeleton is
identical to MCM.
Recurrence Relation
triangulate(i,j) = min over k in (i, j) of triangulate(i,k) + triangulate(k,j) +
values[i]*values[k]*values[j]
C++17 Solution
int minScoreTriangulation(vector<int>& values) {
int n = [Link]();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int len = 2; len < n; len++) {
for (int i = 0; i + len < n; i++) {
int j = i + len;
dp[i][j] = INT_MAX;
for (int k = i + 1; k < j; k++)
dp[i][j] = min(dp[i][j],
dp[i][k] + dp[k][j] + values[i]*values[k]*values[j]);
}
}
return dp[0][n - 1];
}
How to Recognize It
• MCM-style interval DP: for a range [i, j], try every cut point k; if s[i..k] is a palindrome, recurse on the remainder.
Recurrence Relation
minCuts(i,j) = 0 if s[i..j] is a palindrome
minCuts(i,j) = min over k in [i, j-1] of 1 + minCuts(i,k) + minCuts(k+1,j) otherwise
C++17 Solution
bool isPalindrome(string& s, int i, int j) {
while (i < j) if (s[i++] != s[j--]) return false;
return true;
}
int minCutsRec(string& s, int i, int j) {
if (i >= j || isPalindrome(s, i, j)) return 0;
int minCost = INT_MAX;
for (int k = i; k < j; k++) {
if (isPalindrome(s, i, k)) {
int cost = 1 + minCutsRec(s, k + 1, j);
minCost = min(minCost, cost);
}
}
return minCost;
}
Complexity: Exponential without memoization; checking isPalindrome each time adds an extra O(n) factor.
How to Recognize It
• Overlapping subproblems appear because the same (i, j) range is re-examined through different cut sequences.
Recurrence Relation
Same recurrence as 4.3, cached in dp[i][j].
C++17 Solution
vector<vector<int>> memo;
int minCutsMemo(string& s, int i, int j) {
if (i >= j || isPalindrome(s, i, j)) return 0;
if (memo[i][j] != -1) return memo[i][j];
int minCost = INT_MAX;
for (int k = i; k < j; k++)
if (isPalindrome(s, i, k))
minCost = min(minCost, 1 + minCutsMemo(s, k + 1, j));
return memo[i][j] = minCost;
}
Complexity: O(n^3) time (n^2 states × O(n) split loop), O(n^2) space.
How to Recognize It
• Precompute isPal[i][j] using: isPal[i][j] = (s[i]==s[j]) AND (j-i<=2 OR isPal[i+1][j-1]).
Recurrence Relation
Same cut recurrence as 4.3/4.4, but isPalindrome(i,j) is replaced with the O(1) lookup
isPal[i][j].
C++17 Solution
int minCutsOptimized(string& s) {
int n = [Link]();
vector<vector<bool>> isPal(n, vector<bool>(n, false));
for (int len = 1; len <= n; len++)
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (s[i] == s[j] && (len <= 2 || isPal[i+1][j-1])) isPal[i][j] = true;
}
Complexity: O(n^2) time overall (palindrome table O(n^2) + 1-D DP O(n^2)), O(n^2) space.
How to Recognize It
• MCM-style interval DP over the operator positions, but each subrange has TWO answers to track: ways to be
True and ways to be False.
Recurrence Relation
For each operator at position k splitting into left [i,k-1] and right [k+1,j]:
& : True ways += Lt*Rt
| : True ways += Lt*Rt + Lt*Rf + Lf*Rt
^ : True ways += Lt*Rf + Lf*Rt
(False ways = total ways at that split − True ways)
C++17 Solution
// countWays(i, j) returns {waysTrue, waysFalse} for substring i..j (only symbols,
operators are between)
pair<long long,long long> countWaysRec(string& s, int i, int j) {
if (i == j) return { s[i]=='T' ? 1LL : 0LL, s[i]=='F' ? 1LL : 0LL };
long long trueWays = 0, falseWays = 0;
for (int k = i + 1; k < j; k += 2) { // k is always an operator index
auto [lt, lf] = countWaysRec(s, i, k - 1);
auto [rt, rf] = countWaysRec(s, k + 1, j);
long long total = (lt + lf) * (rt + rf);
long long tWays = 0;
if (s[k] == '&') tWays = lt * rt;
else if (s[k] == '|') tWays = lt*rt + lt*rf + lf*rt;
else /* '^' */ tWays = lt*rf + lf*rt;
trueWays += tWays;
falseWays += (total - tWays);
}
return { trueWays, falseWays };
}
How to Recognize It
• State (i, j, wantTrue) — memoize both the True-count and False-count table using a fixed-size 3D array since i, j
are bounded by string length.
Recurrence Relation
Same combine rules as 4.6, but each (i,j,0/1) result is cached instead of recomputed.
C++17 Solution
long long dp3[200][200][2]; // dp3[i][j][1]=true ways, dp3[i][j][0]=false ways
bool computed[200][200];
pair<long long,long long> countWaysMemo(string& s, int i, int j) {
if (i == j) return { s[i]=='T'?1LL:0LL, s[i]=='F'?1LL:0LL };
if (computed[i][j]) return { dp3[i][j][1], dp3[i][j][0] };
long long trueWays = 0, falseWays = 0;
for (int k = i + 1; k < j; k += 2) {
auto [lt, lf] = countWaysMemo(s, i, k - 1);
auto [rt, rf] = countWaysMemo(s, k + 1, j);
long long total = (lt + lf) * (rt + rf);
long long tWays = (s[k]=='&') ? lt*rt
: (s[k]=='|') ? lt*rt+lt*rf+lf*rt
: lt*rf+lf*rt;
trueWays += tWays;
falseWays += total - tWays;
}
computed[i][j] = true;
dp3[i][j][1] = trueWays; dp3[i][j][0] = falseWays;
return { trueWays, falseWays };
}
Complexity: O(n^3) time, O(n^2) space (fixed 3D array bounded by max string length).
How to Recognize It
• Same recurrence as 4.6/4.7; only the memo storage mechanism changes.
Recurrence Relation
Identical recurrence to 4.6, cached in memo[to_string(i)+','+to_string(j)].
C++17 Solution
unordered_map<string, pair<long long,long long>> memo;
pair<long long,long long> countWaysMap(string& s, int i, int j) {
if (i == j) return { s[i]=='T'?1LL:0LL, s[i]=='F'?1LL:0LL };
string key = to_string(i) + "," + to_string(j);
if ([Link](key)) return memo[key];
long long trueWays = 0, falseWays = 0;
for (int k = i + 1; k < j; k += 2) {
auto [lt, lf] = countWaysMap(s, i, k - 1);
auto [rt, rf] = countWaysMap(s, k + 1, j);
long long total = (lt + lf) * (rt + rf);
long long tWays = (s[k]=='&') ? lt*rt
: (s[k]=='|') ? lt*rt+lt*rf+lf*rt
: lt*rf+lf*rt;
trueWays += tWays;
falseWays += total - tWays;
}
return memo[key] = { trueWays, falseWays };
}
How to Recognize It
• MCM-style: try every split point k inside the string; either the halves match without swapping or they match
after swapping.
Recurrence Relation
isScramble(s1,s2) = true if s1==s2
Otherwise: OR over split k of
(isScramble(s1[0..k], s2[0..k]) AND isScramble(s1[k..], s2[k..])) [no swap]
OR (isScramble(s1[0..k], s2[len-k..]) AND isScramble(s1[k..], s2[0..len-k])) [swap]
C++17 Solution
bool isScramble(string s1, string s2) {
if (s1 == s2) return true;
if ([Link]() != [Link]()) return false;
string a = s1, b = s2;
sort([Link](), [Link]()); sort([Link](), [Link]());
if (a != b) return false; // pruning: different character sets
int n = [Link]();
for (int k = 1; k < n; k++) {
// no swap
if (isScramble([Link](0, k), [Link](0, k)) &&
isScramble([Link](k), [Link](k))) return true;
// swap
if (isScramble([Link](0, k), [Link](n - k)) &&
isScramble([Link](k), [Link](0, n - k))) return true;
}
return false;
}
How to Recognize It
• Overlapping subproblems: identical (s1_substring, s2_substring) pairs recur across different split choices.
Recurrence Relation
Same recurrence as 4.9, cached in unordered_map<string, bool> keyed by s1+"_"+s2.
C++17 Solution
unordered_map<string, bool> scrambleMemo;
bool isScrambleMemo(string s1, string s2) {
if (s1 == s2) return true;
if ([Link]() != [Link]()) return false;
string key = s1 + "_" + s2;
if ([Link](key)) return scrambleMemo[key];
int n = [Link]();
for (int k = 1; k < n; k++) {
if (isScrambleMemo([Link](0,k), [Link](0,k)) &&
isScrambleMemo([Link](k), [Link](k)))
return scrambleMemo[key] = true;
if (isScrambleMemo([Link](0,k), [Link](n-k)) &&
isScrambleMemo([Link](k), [Link](0,n-k)))
return scrambleMemo[key] = true;
}
return scrambleMemo[key] = false;
}
Complexity: O(n^4) time (n^2 distinct substring pairs × O(n) split loop × O(n) substr cost), O(n^2) map entries.
How to Recognize It
• State: (e, f) = number of eggs available and number of floors still to check.
• Choice: drop from floor k (1<=k<=f); if it breaks, recurse with (e-1, k-1); if it survives, recurse with (e, f-k). Take
the worst (max) of these two, then the best (min) over all choices of k.
Recurrence Relation
eggDrop(e,f) = min over k in [1,f] of 1 + max( eggDrop(e-1,k-1), eggDrop(e,f-k) )
Base cases: eggDrop(1,f)=f; eggDrop(e,0)=0; eggDrop(e,1)=1
C++17 Solution
int eggDropRec(int e, int f) {
if (f == 0 || f == 1) return f;
if (e == 1) return f;
int minTrials = INT_MAX;
for (int k = 1; k <= f; k++) {
int broken = eggDropRec(e - 1, k - 1);
int survived = eggDropRec(e, f - k);
minTrials = min(minTrials, 1 + max(broken, survived));
}
return minTrials;
}
Recurrence Relation
Same as 5.1, cached in dp[e][f].
C++17 Solution
vector<vector<int>> eggMemo;
int eggDropMemo(int e, int f) {
if (f == 0 || f == 1) return f;
if (e == 1) return f;
if (eggMemo[e][f] != -1) return eggMemo[e][f];
int minTrials = INT_MAX;
for (int k = 1; k <= f; k++) {
int broken = eggDropMemo(e - 1, k - 1);
int survived = eggDropMemo(e, f - k);
minTrials = min(minTrials, 1 + max(broken, survived));
}
return eggMemo[e][f] = minTrials;
}
How to Recognize It
• As k increases, eggDrop(e-1,k-1) increases monotonically and eggDrop(e,f-k) decreases monotonically.
• We are looking for the k where these two curves cross — binary search finds it in O(log f) instead of O(f).
Recurrence Relation
Same recurrence as 5.1/5.2, but the search over k is replaced with binary search on the
monotonic broken/survived curves.
C++17 Solution
vector<vector<int>> eggMemoBS;
int eggDropBinarySearch(int e, int f) {
if (f == 0 || f == 1) return f;
if (e == 1) return f;
if (eggMemoBS[e][f] != -1) return eggMemoBS[e][f];
Complexity: O(e * f * log f) time, O(e*f) space — a large practical improvement, and the version expected for LeetCode
887 (Super Egg Drop) to avoid TLE.
How to Recognize It
• For each node, the longest path through it equals height(left subtree) + height(right subtree).
• Naively recomputing height() for every node gives O(n^2); compute height and update the diameter
simultaneously in one post-order pass to get O(n).
Recurrence Relation
height(node) = 1 + max(height([Link]), height([Link]))
diameter = max over all nodes of (height(left) + height(right))
C++17 Solution
int diameterResult = 0;
int height(TreeNode* node) {
if (!node) return 0;
int leftHeight = height(node->left);
int rightHeight = height(node->right);
diameterResult = max(diameterResult, leftHeight + rightHeight);
return 1 + max(leftHeight, rightHeight);
}
int diameterOfBinaryTree(TreeNode* root) {
diameterResult = 0;
height(root);
return diameterResult;
}
Complexity: O(n) time (single post-order traversal), O(h) space for the recursion stack (h = tree height).
How to Recognize It
• Similar to diameter, but instead of heights we track the maximum single-branch sum a node can contribute
upward, while updating a global best using both branches at once.
Recurrence Relation
maxSingleBranch(node) = [Link] + max(0, maxSingleBranch(left), maxSingleBranch(right))
globalBest = max(globalBest, [Link] + max(0,left) + max(0,right))
C++17 Solution
int globalBest = INT_MIN;
int maxSingleBranch(TreeNode* node) {
if (!node) return 0;
int left = max(0, maxSingleBranch(node->left));
int right = max(0, maxSingleBranch(node->right));
globalBest = max(globalBest, node->val + left + right);
return node->val + max(left, right);
}
int maxPathSum(TreeNode* root) {
globalBest = INT_MIN;
maxSingleBranch(root);
return globalBest;
}
How to Recognize It
• Same idea as 6.2, but a node can only contribute to the global answer through two children if BOTH children
exist (a true leaf-to-leaf path can't 'turn' at a node with only one child).
Recurrence Relation
If a node has both children: globalBest considers left + [Link] + right.
If a node has only one child, the path must continue through that one child only (no
turning).
C++17 Solution
int leafBest = INT_MIN;
int maxLeafBranch(TreeNode* node) {
if (!node) return 0;
if (!node->left && !node->right) return node->val; // true leaf
0/1 Knapsack Choose items once each to hit a Knapsack, Subset Sum, Target Sum
target/capacity
Unbounded Knapsack Items may repeat unlimited times Rod Cutting, Coin Change
LCS / Two-Pointer String DP Compare two sequences with LCS, SCS, Edit Distance, LPS
match/skip choices
MCM / Interval (Partition) DP Choose a split point k inside a range MCM, Palindrome Partitioning,
Boolean Parenthesization, Scramble
String
Egg Drop style DP Minimize worst-case trials under Egg Dropping, Binary Search
constraints optimization
4. Memoization Template
unordered_map<long long, int> memo; // or vector<vector<int>> for bounded states
5. Tabulation Template
vector<vector<int>> dp(N + 1, vector<int>(M + 1, 0));
return dp[N][M];