DP interview problems boil down to 5 core patterns: Knapsack, Sequence, Interval, Grid, and Partition.
Pattern recognition beats memorization – identify the recurrence shape first.
Space optimization (1D vs 2D) is the most common follow-up; practice it.
O(N) space is often possible; interviewers check if you see it.
Biggest mistake: jumping to code before defining the state and transition explicitly.
Plain-English First
Imagine you're hiking a mountain and at every fork you have two paths. A rookie hiker tries every possible route from scratch each time. A smart hiker writes down 'fork 3 took 20 minutes' in a notebook so they never retrace those steps again. Dynamic programming is that notebook — it's the art of solving a big problem by solving smaller versions of it once, remembering the answer, and reusing it instead of recalculating. The 'dynamic' part just means the problem has choices that build on each other.
Dynamic programming separates the candidates who get offers from the ones who get 'we'll be in touch.' It's not because DP is obscure — it's because it forces you to think recursively, spot structure in chaos, and optimise on the fly. Every major tech company — Google, Meta, Amazon, Microsoft — leans on DP problems to stress-test exactly those skills. If you can solve a DP problem cleanly under pressure, you've signalled that you understand trade-offs, not just syntax.
The reason DP trips people up isn't the math — it's the pattern recognition. Beginners see 20 different DP problems and treat each one as a unique puzzle. Senior engineers see the same 5 underlying patterns wearing different costumes. Once you can identify 'this is a knapsack variant' or 'this is interval DP,' you're not solving problems cold anymore — you're applying a playbook. That shift from memorisation to pattern recognition is exactly what this article builds.
By the end of this article you'll be able to recognise the 10 most common DP problem archetypes, implement each one from scratch with correct base cases and transition functions, explain your space-optimisation decisions out loud, and handle the follow-up questions interviewers use to probe whether you truly understand or just memorised. Let's build that playbook.
DP Pattern Matrix for Rapid Classification
When you see a new problem, ask yourself three questions: What is the state? What is the recurrence shape? What are the base cases? These form a pattern matrix that maps every DP problem to one of five archetypes. Use the table below to classify within seconds.
Pattern
State Definition
Transition Shape
Typical Base Case
0/1 Knapsack
dp[i][w] = max value using first i items, capacity w
dp[w] = min/max value for capacity w (item index not needed)
dp[w] = min(dp[w], dp[w-coin] + 1) forward loop
dp[0]=0, rest = INF
Sequence (LCS/Edit Distance)
dp[i][j] = answer for prefix i of string1, prefix j of string2
if match: diag+1; else: max(left,up) or min of three edits
dp[0][]= , dp[][0]=
Interval DP
dp[i][j] = answer for substring s[i..j] or matrix chain (i..j)
dp[i][j] = min over k of (dp[i][k] + dp[k+1][j] + cost)
dp[i][i]=0, dp[i][i+1]=cost
Grid DP
dp[i][j] = ways/cost to reach cell (i,j) from (0,0)
dp[i][j] = dp[i-1][j] + dp[i][j-1] (or + cost)
first row/col init to 1 or cost
LIS (Patience)
tails[l] = smallest tail of an increasing subsequence of length l
binary search tails to place x
tails[0]=first element
To use the matrix: Identify the structure of the input. If you see two sequences → Sequence or Edit Distance. If you see a single array with capacity limit → Knapsack. If you see substrings/ranges → Interval. If you see an n×m grid → Grid. If you see a problem asking for the longest increasing order → LIS. This classification step alone eliminates 80% of the guesswork.
Print out this pattern matrix and stick it on your whiteboard during interview prep. The moment you can name the pattern, you already know the recurrence.
Production Insight
In production, you rarely encounter textbook DP. But the pattern matrix still applies: if you have a resource allocation problem (capacity + items) use knapsack; if you have a matching problem (two sequences) use sequence DP; if you have a scheduling problem with intervals use interval DP. Always start by classifying before coding.
Key Takeaway
Classifying the problem using this matrix is the fastest route to the correct recurrence.
Visual State Transition Table Walkthrough
One of the best ways to internalize DP is to draw the state transition table manually. Let's take the classic "Coin Change 2" problem (number of combinations to make an amount using unlimited coins) and walk through the table cell by cell. We'll use a small example: coins = [1,2,5], amount = 5. The DP table is built row by row, where each row represents a coin and each column represents an amount. The value in dp[coinIndex][amount] is the number of combinations using only coins up to the current coin type.
We'll trace the first two rows
Row 0 (coin=1): For amounts 0..5, there's exactly 1 way (all 1s). Fill accordingly.
Row 1 (coin=2): For amount j, dp[j] = dp_prevRow[j] + dp_currentRow[j-2] (because we can either exclude coin 2 or include it and look at current row for remaining amount). This forward loop ensures we can use multiple 2s.
Below is a step-by-step visualization using array states for each row. Watch how the values evolve.
publicclassCoinChange2TableWalkthrough {
publicintchange(int amount, int[] coins) {
int[] dp = newint[amount + 1];
dp[0] = 1;
for (int coin : coins) {
for (int x = coin; x <= amount; x++) {
dp[x] = dp[x] + dp[x - coin]; // exclude + include
}
}
return dp[amount];
}
}
Output
// For coins = [1,2,5], amount = 5 -> Output: 4
Trace the Table Yourself
Take a piece of paper, draw a 3×6 grid (coins vs amount). Fill each cell manually. You'll notice the recurrence pattern: new = old + left-same-row. This hands-on exercise makes the recurrence unforgettable.
Production Insight
When debugging DP in production, printing the DP table for a small input is often the fastest way to spot off-by-one errors. If the output is wrong, log the entire table for a tiny test case and compare with your hand-drawn version. The eye catches mismatches quickly.
Key Takeaway
Visualizing the state transition table teaches you the recurrence from the ground up — you stop guessing and start seeing.
Memoization vs Tabulation Comparison Matrix
Both memoization (top-down) and tabulation (bottom-up) solve the same DP problems but they differ in performance, memory, and safety. The table below compares them across key dimensions.
Dimension
Memoization (Top-Down)
Tabulation (Bottom-Up)
Ease of implementation
Easier if you already have a recursive solution
Requires iterative loops; risk of index off-by-one
Time complexity
Same O(state space * transition cost) but function call overhead
Usually faster due to cache-friendly loops
Space complexity
O(states) for memo map + call stack O(depth) – may be O(N) extra
Typically uses explicit DP array – can be optimized to O(1) or O(N)
Stack safety
Risk of StackOverflow for large depth (N > 1000)
No recursion, always stack safe
Pruning efficiency
Naturally prunes unreachable states – only computes needed states
Always iterates over full state space – can be wasteful
Optimal substructure clarity
Very clear: recursion mirrors the recurrence directly
Often hidden in loops
Space optimization
Hard to reduce space from 2D to 1D (still need map)
Easy to switch to 1D by replacing 2D array with rolling arrays
When to use which? For interviews: I recommend starting with tabulation if you can see the bottom-up order. It's safer and faster. Use memoization as a fallback when the state transition order is not obvious (e.g., string-based state). In production, always prefer tabulation to avoid stack overflow and achieve better cache locality. But for complex state definitions (like dictionary words), memoization with HashMap can be cleaner.
Below is a code example of Fibonacci implemented both ways.
io/thecodeforge/dp/FibonacciComparison.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
publicclassFibonacciComparison {
// Memoizationstaticint[] memo;
publicstaticintfibMemo(int n) {
if (n <= 1) return n;
if (memo[n] != 0) return memo[n];
memo[n] = fibMemo(n-1) + fibMemo(n-2);
return memo[n];
}
// TabulationpublicstaticintfibTab(int n) {
if (n <= 1) return n;
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int cur = prev1 + prev2;
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
publicstaticvoidmain(String[] args) {
int n = 100;
memo = newint[n+1];
System.out.println("Memo: " + fibMemo(n)); // could stack overflow for n=10000System.out.println("Tab: " + fibTab(n)); // safe
}
}
Output
Memo: 354224848179261915075 (for n=100)
Tab: 354224848179261915075
Stack Depth Danger in Interviews
If n=10000, fibMemo will overflow most JVM stacks (default ~1MB). Always mention this trade-off in interviews. Tabulation is your safety net.
Production Insight
Our production incident (StackOverflowError) happened because the team used memoization without realizing the recursion depth could hit 5000. If you must use top-down in a critical path, set -Xss2m and implement a depth counter with graceful fallback to iterative DP. Better yet: never ship recursive DP to production.
Key Takeaway
Use tabulation for performance and safety; use memoization only when the state space is naturally pruned or the recurrence is too complex to iterate.
Space Optimization: From O(N²) to O(N)
One of the most common follow-up questions in interviews is: "Can you optimize the space from O(N²) to O(N)?" This is not a trick — it's a test of your understanding of the dependency pattern in the recurrence. If the current state depends only on the previous row (or a fixed number of previous states), you can reduce space by storing only those rows.
General technique: Replace the 2D DP table with a 1D array and update it in-place. The iteration order (forward vs backward) determines whether it's unbounded or 0/1. For LCS (two sequences), you need two rows because you need both dp[i-1][j] and dp[i][j-1] and dp[i-1][j-1]. Use a temporary variable to preserve the old diagonal value.
Example: 0/1 Knapsack — original 2D table O(NC). Observe that dp[i][w] only uses dp[i-1][] (row above). So we can use a 1D array and iterate capacity backwards to avoid overwriting needed values. This is the most famous space optimization.
Example: Longest Common Subsequence — 2D O(mn). Since transition uses dp[i-1][j-1], dp[i-1][j], and dp[i][j-1], we need to keep two rows (previous and current) and a variable to remember the old diagonal. This reduces space to O(2min(m,n)) = O(min(m,n)).
Example: Grid DP (Unique Paths) — only need one row because dp[j] = dp[j] (above) + dp[j-1] (left). Update left to right.
When space optimization is NOT possible: Interval DP requires all intervals of different lengths; the dependency is not just on previous row. Burst Balloons and Matrix Chain Multiplication typically remain O(N²) space. However, you can sometimes reduce by dividing into halves but that's advanced.
Below is a side-by-side implementation showing the progression from O(N²) to O(N) for LCS.
io/thecodeforge/dp/LcsSpaceOptimized.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
publicclassLcsSpaceOptimized {
// O(m*n) time, O(min(m,n)) spacepublicintlcsOptimized(String text1, String text2) {
if (text1.length() < text2.length()) {
String tmp = text1; text1 = text2; text2 = tmp;
}
int m = text1.length(), n = text2.length();
int[] prev = new int[n+1]; // previous row
int[] cur = new int[n+1]; // current rowfor (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i-1) == text2.charAt(j-1)) {
cur[j] = 1 + prev[j-1];
} else {
cur[j] = Math.max(prev[j], cur[j-1]);
}
}
// swap rowsint[] temp = prev;
prev = cur;
cur = temp;
// clear cur for next iteration (optional, as values will be overwritten)Arrays.fill(cur, 0);
}
return prev[n];
}
}
For N=1000, a 2D int array is ~4MB, a 1D array is ~4KB. In a cloud microservice with multiple concurrent requests, this can save gigabytes of memory.
Production Insight
Our production scheduler incident was triggered by stack depth, but even after converting to bottom-up, the original 2D allocation of dp[m+1][n+1] would have been 40MB for m=n=100000. We used space optimization to keep it under 1MB. Always consider the maximum input size before committing to a DP array dimension. For memory-constrained environments (e.g., mobile, embedded), every byte counts.
Key Takeaway
Space optimization from O(N²) to O(N) is possible when the recurrence only depends on the previous state(s). Master it for Knapsack (backward loop), LCS (two rows + diagonal variable), and Grid DP (one row).
Core Pattern: The 0/1 Knapsack Framework
The most pervasive pattern in DP is the 'Selection' problem, exemplified by the 0/1 Knapsack. In this scenario, you face a series of items and must decide: 'Do I include this item or skip it?' This binary choice builds a decision tree that we optimize using a 2D array (or a space-optimized 1D array). Understanding this state transition is the key to solving variations like 'Partition Equal Subset Sum' and 'Target Sum.'
io/thecodeforge/dp/KnapsackSolver.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package io.thecodeforge.dp;
publicclassKnapsackSolver {
/**
* Standard0/1KnapsackImplementation
* TimeComplexity: O(N * W), SpaceComplexity: O(W)
*/
publicintsolveKnapsack(int[] values, int[] weights, int capacity) {
int n = values.length;
// Space optimized 1D DP arrayint[] dp = newint[capacity + 1];
for (int i = 0; i < n; i++) {
// Iterate backwards to ensure we don't use the same item twicefor (int w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], values[i] + dp[w - weights[i]]);
}
}
return dp[capacity];
}
}
Output
// Given weights [2, 3, 4] and values [3, 4, 5] with capacity 5 -> Output: 7
Forge Tip:
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick. When explaining this in an interview, emphasize that the 1D array optimization works because we only need values from the previous 'item' row.
Production Insight
Forgetting to iterate capacity backwards is the #1 bug – leads to using the same item multiple times.
If capacity is large (10^6) and N is small, this algorithm is still fine; if both are large, consider branch and bound or meet-in-the-middle.
Always validate capacity <= 10^5 for O(N*C) to avoid TLE in interviews.
Key Takeaway
0/1 Knapsack = binary choice per item, backwards loop for capacity.
Space optimization from O(N*W) to O(W) is expected – don't skip it.
Master this pattern and you unlock Subset Sum, Target Sum, and Partition Equal Subset Sum.
Choosing Space Optimization Strategy
IfCapacity <= 10^4 and N <= 100
→
UseUse 2D array for clarity; space is acceptable.
IfCapacity up to 10^5, N up to 500
→
UseUse 1D array (space-optimized) – critical for passing.
IfCapacity is huge (10^9) or unconstrained
→
UseNot a classic knapsack – look for other patterns or meet-in-the-middle.
Sequence Pattern: Longest Common Subsequence (LCS)
When dealing with two strings or arrays, the 'Sequence' pattern is your go-to. The goal is to find the relationship between two prefixes. The transition function relies on a simple logic: if characters match, extend the previous subsequence; if they don't, take the best result from either ignoring the current character of the first string or the second.
io/thecodeforge/dp/LcsService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.thecodeforge.dp;
publicclassLcsService {
/**
* io.thecodeforge: StandardLCS implementation
* Handles the core of problems like 'Edit Distance' and 'Longest Palindromic Subsequence'
*/
publicintfindLCS(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = newint[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = 1 + dp[i - 1][j - 1];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
}
Output
// Input: "abcde", "ace" -> Output: 3
Interview Strategy:
Interviewers often follow up by asking you to print the actual string, not just the length. Always be prepared to backtrack through your DP table to reconstruct the solution path.
Production Insight
Space optimization for LCS reduces to 2 rows (O(2*min(m,n))) – practice it.
When strings are very long (DNA sequences), O(m*n) might be too heavy; look for Hirschberg's algorithm for linear space.
Reconstruction is often ignored in interviews – but being able to backtrack from dp[m][n] shows real mastery.
Key Takeaway
LCS transition: match -> 1+diagonal, mismatch -> max(left, up).
Space can be O(min(m,n)) with two rows – standard follow-up.
This pattern extends to Edit Distance (add cost), LCS with three strings, and Longest Palindromic Subsequence (LCS of string and reverse).
Unbounded Knapsack: Coin Change & Rod Cutting
In the Unbounded Knapsack, you have unlimited copies of each item. The recurrence changes: when you include an item, you stay on the same item row instead of moving to the previous one. This means the inner loop iterates forward. Classic problems: Coin Change (minimum coins), Coin Change 2 (number of combinations), and Rod Cutting.
io/thecodeforge/dp/CoinChangeSolver.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package io.thecodeforge.dp;
publicclassCoinChangeSolver {
/**
* Minimum number of coins to make amount.
* Unbounded knapsack – iterate forward.
*/
publicintcoinChange(int[] coins, int amount) {
int[] dp = newint[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int coin : coins) {
for (int x = coin; x <= amount; x++) {
dp[x] = Math.min(dp[x], 1 + dp[x - coin]);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
Interval DP solves problems by considering all subarrays or substrings. The state is typically defined by the start and end indices of a range. The transition tries every possible split point within that range and combines the results. Classic problems: Matrix Chain Multiplication, Palindromic Substrings, Burst Balloons, and Optimal BST.
package io.thecodeforge.dp;
publicclassMatrixChainMultiplication {
/**
* Compute minimum multiplication cost for matrices A1A2...An
*/
publicintminCost(int[] dimensions) {
int n = dimensions.length - 1; // number of matricesint[][] dp = newint[n][n];
// length L from 2 to nfor (int L = 2; L <= n; L++) {
for (int i = 0; i <= n - L; i++) {
int j = i + L - 1;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k+1][j] + dimensions[i]*dimensions[k+1]*dimensions[j+1];
dp[i][j] = Math.min(dp[i][j], cost);
}
}
}
return dp[0][n-1];
}
}
Output
// dimensions = [10, 30, 5, 60] -> Output: 4500
Think of it as: 'solve substring, then glue'
State = (i,j) representing a contiguous segment.
Base case: single matrix or empty substring (cost 0).
Transition: iterate over partition point k, combine results from (i,k) and (k+1,j).
Always compute smaller intervals first (increasing length).
Production Insight
O(n^3) time is typical – n up to 500 is fine (~125M operations), but above 1000 it's risky.
Space optimization for interval DP rarely reduces dimension because you need all intervals.
Burst Balloons is a tricky variant: add dummy balloons with value 1 to simplify base cases.
String palindrome problems: 2D DP to store true/false for palindromes, then compute min cuts or count.
Key Takeaway
Interval DP = len loop from 2 to n, then i, then k split.
Recurrence: dp[i][j] = min over k of (dp[i][k] + dp[k+1][j] + cost to combine).
Works for any problem that asks 'optimal way to process a range'.
Grid DP: Unique Paths & Minimum Path Sum
Grid DP problems involve moving through a matrix from top-left to bottom-right, often with obstacles or costs. The state is (row, col) and transitions come from the left or above. This is one of the easiest DP patterns to recognize – the recurrence is straightforward – but variations like 'Dungeon Game' and 'Cherry Pickup' push it to the next level.
io/thecodeforge/dp/UniquePaths.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package io.thecodeforge.dp;
publicclassUniquePaths {
/**
* Robot from top-left to bottom-right, only down or right.
*/
publicintuniquePaths(int m, int n) {
int[] dp = newint[n];
Arrays.fill(dp, 1);
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j - 1]; // dp[j] from above + left
}
}
return dp[n-1];
}
}
Output
// m=3, n=7 -> Output: 28
Space optimization trick:
You only need one array (1D) for grid DP because you compute row by row, storing the previous row and updating left to right.
Production Insight
For large grids (m,n up to 1000), O(m*n) is fine. But if both are 10^5, use combinatorial formula (C(m+n-2, m-1)).
When obstacles exist (Unique Paths II), the transition becomes: if grid[i][j] is obstacle, dp[j]=0; else dp[j] += dp[j-1].
Minimum Path Sum with negative costs works the same, but you need to handle the first row/col carefully to avoid taking unreachable paths.
Key Takeaway
Grid DP recurrence: dp[i][j] = ways from above + ways from left.
Longest Increasing Subsequence (LIS) – The O(n log n) Variant
LIS is a classic that tests your ability to go beyond O(n^2). The DP solution is O(n^2), but the optimal solution uses patience sorting (binary search) to achieve O(n log n). The state is the smallest possible tail of an increasing subsequence of each length. This pattern reappears in problems like 'Russian Doll Envelopes' and 'Maximum Length of Pair Chain'.
io/thecodeforge/dp/LIS.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package io.thecodeforge.dp;
publicclassLIS {
publicintlengthOfLIS(int[] nums) {
int[] tails = newint[nums.length];
int size = 0;
for (int x : nums) {
int i = Arrays.binarySearch(tails, 0, size, x);
if (i < 0) i = -(i + 1);
tails[i] = x;
if (i == size) size++;
}
return size;
}
}
Output
// nums = [10,9,2,5,3,7,101,18] -> Output: 4
Don't confuse LIS with longest increasing contiguous subarray
LIS allows non-contiguous elements. The O(n log n) solution does not give the subsequence itself – only length. To reconstruct, use parent pointers or store indices.
Production Insight
O(n log n) is essential for inputs with n up to 10^5 – the O(n^2) version would TLE.
The 'tails' array is not the actual LIS sequence – it only stores the minimum tail values. Many interviewees mistakenly think they can read it back.
For problems like 'Maximum Length of Pair Chain', you must sort by first element then apply LIS logic on the second element.
Binary search must use Arrays.binarySearch on the active portion – using the full tail array leads to wrong results.
Key Takeaway
LIS O(n log n) = maintain tails array, binary search to find place for each new element.
tails[i] = smallest possible tail of a length-(i+1) increasing subsequence.
Master this and you unlock the 'patience sorting' pattern – it's the foundation of many sorting/DP hybrids.
Edit Distance measures how many single-character edits (insert, delete, replace) are needed to transform one string into another. The recurrence is the same as LCS but with an extra operation (replace). It's a classic DP problem that tests your ability to handle three transitions correctly. Variations include one-edit-distance, and weighted edit operations.
io/thecodeforge/dp/EditDistance.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.thecodeforge.dp;
publicclassEditDistance {
publicintminDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = newint[m+1][n+1];
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i-1) == word2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1]; // match
} else {
dp[i][j] = 1 + Math.min(
dp[i-1][j], // deleteMath.min(dp[i][j-1], // insert
dp[i-1][j-1])); // replace
}
}
}
return dp[m][n];
}
}
Output
// word1 = "horse", word2 = "ros" -> Output: 3
Memory Optimization:
You can reduce space to O(min(m,n)) using two rows, but be careful with the diagonal (dp[i-1][j-1]) which requires saving the previous j-1 value before overwriting.
Production Insight
Edit Distance is often used in auto-correct and DNA sequence alignment – performance matters.
O(m*n) time is fine for strings up to 10^3 each. For longer strings, use Hirschberg's algorithm (linear space, same time).
In interviews, expect follow-ups: what if each operation has different costs? Then the recurrence changes: dp[i][j] = min(cost_delete + dp[i-1][j], cost_insert + dp[i][j-1], cost_replace + dp[i-1][j-1]).
Key Takeaway
Edit Distance = LCS with extra replace operation.
Base: one empty string requires all insertions/deletions.
Space can be O(min(m,n)) with careful diagonal handling.
If characters match, cost is 0 (no edit) – use diagonal value.
● Production incidentPOST-MORTEMseverity: high
The Recursive DP That Took Down a Production Scheduler
Symptom
java.lang.StackOverflowError in production after ~5000 recursive calls. The DP function worked in tests with small inputs.
Assumption
The recursion depth would stay shallow because the problem size was small (N≤50). But the scheduler ran in a loop processing thousands of tasks.
Root cause
Top-down recursion without proper iterative fallback. Each recursive call consumed stack frame, and the JVM stack size was not increased. N=5000 caused overflow.
Fix
Rewrote the DP using bottom-up tabulation. Applied space optimization to use 1D array. Set JVM -Xss2m for legacy top-down code as backup.
Key lesson
Always estimate maximum recursion depth before shipping top-down DP to production.
Bottom-up DP is safer for production – no stack overflow, better cache locality.
If you must keep top-down, increase stack size and add a depth limiter with fallback.
Production debug guideQuick diagnosis for common DP failures in live systems4 entries
Symptom · 01
StackOverflowError in recursive DP
→
Fix
Check input size – if N > 1000, switch to bottom-up immediately. Add explicit stack limit or use iterative approach.
Symptom · 02
OutOfMemoryError with 2D DP array
→
Fix
Identify if only previous row matters – if yes, reduce to 1D array. If both dimensions large, consider sparse representation or top-down with memoization.
Symptom · 03
Wrong answer but passes sample tests
→
Fix
Print DP table for a small input – verify base case and transition logic. Common off-by-one: forgetting capacity 0 or empty string base.
Symptom · 04
Performance degrades with increasing input size quadratically
→
Fix
Check if loops iterate over all states. If state space can be pruned (e.g. knapsack with capacity constraint), use early break or branch and bound.
★ DP Interview Quick Debug Cheat SheetThe top 3 mistakes that crash your DP solution in an interview – and how to fix them on the spot.
Infinite loop / recursion never terminates−
Immediate action
Pause and check base case – does your recursion have a stopping condition? Print a debug log in the function.
Commands
System.out.println("state: " + i + ", " + j);
Verify memoization map is being used – ensure you store result before returning.
Fix now
Add base case at top: if(i==0 || j==0) return 0;. Use == not equals for primitive comparison.
Array index out of bounds in bottom-up DP+
Immediate action
Check dimension sizes vs loop bounds. Common: dp[m+1][n+1] but loop from 0 to m – correct is 1 to m.
Commands
Print loop indexes and array lengths before the access.
Trace a simple input manually on paper and compare with your indices.
Fix now
Ensure dp array size is (m+1) x (n+1) and loops start at 1.
Result is 0 or Integer.MIN_VALUE when expected positive+
Immediate action
Check initialization – did you set dp[0][0] = 0? For maximum problems, initialize with -INF.
Commands
Print the first few rows of the DP table.
Verify transition uses Math.max or Math.min correctly and that base values are right.
Fix now
Initialize dp with -INF for max problems: Arrays.fill(dp, Integer.MIN_VALUE); dp[0]=0;
DP Category
Common Problems
Core Transition Logic
0/1 Knapsack
Subset Sum, Target Sum, Partition Equal Subset Sum
Exclude vs. Include (max(dp[i-1][w], val + dp[i-1][w-wt]))
Unbounded Knapsack
Coin Change, Coin Change 2, Rod Cutting
Include multiple times (forward loop: dp[w] = min(dp[w], 1+dp[w-wt]))
Sequence DP (LCS)
Edit Distance, Longest Palindromic Subsequence, Longest Common Substring
Match (1+dp[i-1][j-1]) vs Mismatch (max of dp[i-1][j], dp[i][j-1])
Identifying the state and transition is 80% of the work. Write it down before coding.
4
Space optimization from O(N^2) to O(N) is a common follow-up
practice it on every pattern.
5
Print the DP table for small inputs to debug; it reveals off-by-one errors instantly.
6
Memorize the loop direction
backward for 0/1, forward for unbounded, length for interval.
Common mistakes to avoid
4 patterns
×
Failing to identify the base cases properly
Symptom
StackOverflow (top-down) or IndexOutOfBounds (bottom-up) when input is empty or single element.
Fix
Always write base cases first: dp[0] = 0 for empty state; for strings, dp[0][j] = j and dp[i][0] = i.
×
Not optimizing space when only the previous row/state is needed
Symptom
Memory usage spikes and candidate gets flagged for suboptimal solution.
Fix
Identify if transition only depends on previous row (e.g., knapsack, grid). Use 1D array and iterate backwards or forwards accordingly.
×
Over-complicating the state – using 4 dimensions when 2 suffice
Symptom
DP table is too large to compute in time, or code is unreadable.
Fix
Simplify subproblems: can you reduce state by swapping indices? E.g., in LCS, use dp[i][j] not dp[i][j][k]. Define state clearly on paper before coding.
×
Forgetting to handle capacity/amount sentinel for 'impossible' cases
Symptom
Returns Integer.MAX_VALUE or 0 instead of -1 when no solution exists.
Fix
Initialize with a large sentinel (e.g., amount+1) and check after fill: if dp[amount] > amount, return -1.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Given an array of integers, find the maximum sum of a non-adjacent subse...
Q02SENIOR
Explain the difference between 0/1 Knapsack and Unbounded Knapsack. How ...
Q03SENIOR
You are given a 2D grid with obstacles. Find the number of unique paths ...
Q04SENIOR
Given a string s, find the longest palindromic subsequence. Explain the ...
Q01 of 04SENIOR
Given an array of integers, find the maximum sum of a non-adjacent subsequence. (House Robber variant). Walk through the recurrence and optimize space.
ANSWER
This is a classic DP problem. Define dp[i] = max sum up to house i. Recurrence: dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Base: dp[0]=nums[0], dp[1]=max(nums[0], nums[1]). Space can be optimized to O(1) by keeping only prev and prevprev variables. For circular houses, run same process twice (skip first and last separately) and take max.
``
public int rob(int[] nums) {
if (nums.length == 0) return 0;
int prev2 = 0, prev = 0;
for (int num : nums) {
int temp = prev;
prev = Math.max(prev, prev2 + num);
prev2 = temp;
}
return prev;
}
``
Q02 of 04SENIOR
Explain the difference between 0/1 Knapsack and Unbounded Knapsack. How does the direction of inner loop change and why?
ANSWER
In 0/1 Knapsack, each item can be used at most once. To avoid using an item more than once, we iterate the inner loop (capacity) backwards. This ensures that when we update dp[w], the dp[w - wt] we read is from the previous item iteration (i-1), not the current one. In Unbounded Knapsack, we want to allow using the same item multiple times, so we iterate the inner loop forward, allowing dp[w - wt] to include the current item already, effectively reusing it.
This direction difference is the key to all knapsack variants. If you reverse the direction in an unbounded problem, you'll get the wrong answer (each item used only once). If you go forward in a 0/1 problem, you'll overcount items.
Q03 of 04SENIOR
You are given a 2D grid with obstacles. Find the number of unique paths from top-left to bottom-right. What if you can also move down, right, and diagonally? How does the state change?
ANSWER
For up/down/right only, use a 2D DP where dp[i][j] = dp[i-1][j] + dp[i][j-1] (with obstacles: set dp[i][j]=0 if obstacle). If diagonal moves are allowed, add + dp[i-1][j-1]. If all 8 directions, the problem becomes a graph BFS not DP, because paths can revisit cells (cycles). The key is to distinguish between directed acyclic paths (DP) and undirected (need BFS/DFS). For unique paths with obstacles, initialize first row and column carefully: once you hit an obstacle, all following cells in that row/col are unreachable (dp=0).
Code for standard unique paths with obstacles:
``
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length, n = obstacleGrid[0].length;
int[] dp = new int[n];
dp[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0;
} else if (j > 0) {
dp[j] += dp[j-1];
}
}
}
return dp[n-1];
}
``
Q04 of 04SENIOR
Given a string s, find the longest palindromic subsequence. Explain the interval DP approach and any optimization.
ANSWER
We can use LCS on s and its reverse, or interval DP directly. Using interval DP: dp[i][j] = length of LPS in substring s[i..j]. Recurrence: if s[i]==s[j], dp[i][j] = 2 + dp[i+1][j-1] (if i+1<=j-1 else 1). Otherwise, dp[i][j] = max(dp[i+1][j], dp[i][j-1]). Base: dp[i][i]=1. This is O(n^2) time and O(n^2) space. Space can be optimized to O(n) using two rows (since dp[i+1][j-1] is the tricky part – we need to store previous diagonal). The optimized version uses a single array and a variable to hold the value of dp[i+1][j-1] before overwriting.
Alternative: Use LCS between s and its reverse – same result, and you can reuse the LCS code from earlier.
01
Given an array of integers, find the maximum sum of a non-adjacent subsequence. (House Robber variant). Walk through the recurrence and optimize space.
SENIOR
02
Explain the difference between 0/1 Knapsack and Unbounded Knapsack. How does the direction of inner loop change and why?
SENIOR
03
You are given a 2D grid with obstacles. Find the number of unique paths from top-left to bottom-right. What if you can also move down, right, and diagonally? How does the state change?
SENIOR
04
Given a string s, find the longest palindromic subsequence. Explain the interval DP approach and any optimization.
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
When should I use Top-Down vs. Bottom-Up DP?
Top-Down (Memoization) is often more intuitive as it follows the natural recursive structure of the problem. However, Bottom-Up (Tabulation) is generally faster in practice because it avoids function call overhead and is easier to space-optimize from $O(N^2)$ to $O(N)$.
Was this helpful?
02
How do I recognize a problem can be solved with Dynamic Programming?
Look for two key properties: 'Overlapping Subproblems' (you find yourself calculating the same result multiple times) and 'Optimal Substructure' (the global solution can be built from optimal solutions of its parts).
Was this helpful?
03
What is the difference between 0/1 Knapsack and Unbounded Knapsack?
In 0/1 Knapsack, you have a limited supply (each item used once), so you iterate through capacities backwards to avoid 'double counting.' In Unbounded Knapsack, you have infinite supply, so you iterate forward to allow an item to be picked multiple times for the same state.
Was this helpful?
04
How do I optimize space in DP from 2D to 1D?
If the transition only depends on the previous row, you can use a single array and update it in-place. For 0/1 Knapsack, iterate capacity backwards. For LCS, use two rows (previous and current) or one row with a temporary variable for the diagonal. Practice this on all patterns.