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.
✦ Definition~90s read
What is Top 10 DP Interview Problems?
Dynamic Programming interview problems test your ability to decompose complex optimization tasks into overlapping subproblems with optimal substructure — essentially, can you recognize when brute-force recursion is wasteful and systematically cache results to avoid recomputation. These problems are the gatekeepers for senior roles at FAANG and top-tier companies because they separate engineers who hack together solutions from those who reason about time/space tradeoffs under constraints.
★
Imagine you're hiking a mountain and at every fork you have two paths.
The core challenge isn't implementing DP itself — it's pattern recognition: identifying whether a problem fits 0/1 Knapsack, Longest Common Subsequence, or Unbounded Knapsack variants, then choosing between top-down memoization (easier to derive, risk of stack overflow in recursion depth) and bottom-up tabulation (harder to conceptualize, but avoids recursion limits and enables space optimization). In production, you'd rarely write DP from scratch — you'd reach for libraries like Google OR-Tools or specialized solvers — but interviewers use DP to probe your algorithmic maturity, specifically your ability to model state transitions and prune memory from O(N²) to O(N) using rolling arrays.
The 0/1 Knapsack framework is the Rosetta Stone here: once you internalize its state table (items vs capacity) and the decision tree of 'take or skip,' you can map it to subset sum, partition equal subset sum, and even some string DP problems. Avoid the trap of memorizing solutions — instead, practice drawing the state transition table manually until the recurrence relation becomes instinctive, because in an interview, your explanation of why you chose memoization over tabulation (e.g., 'the recursion depth is bounded by N=1000, so stack overflow isn't a risk, and memoization lets me prune invalid states early') demonstrates the production-aware thinking they're actually evaluating.
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.
What DP Interview Problems Actually Test
Dynamic programming interview problems test your ability to decompose a problem into overlapping subproblems and reuse computed results. The core mechanic is memoization or tabulation — storing solutions to subproblems so you don't recompute them. This turns exponential brute force into polynomial time, typically O(n²) or O(n·m).
The key property that matters in practice is optimal substructure: the optimal solution to the whole problem must contain optimal solutions to its subproblems. Without that, DP won't work. You also need overlapping subproblems — if subproblems are unique, DP gives no benefit over divide-and-conquer. Recognizing these two properties is the difference between solving a problem and guessing at a recurrence.
Use DP when you see 'count ways', 'minimum cost', 'maximum profit', or 'longest subsequence' — these almost always hide overlapping subproblems. In real systems, DP powers route optimization, resource allocation, and sequence alignment. Knowing when to apply it separates engineers who write O(2^n) code from those who ship production-grade solutions.
Don't Confuse DP with Greedy
Greedy picks the local optimum and hopes it's global. DP explores all options but prunes via memoization. If you use greedy on a DP problem, you'll get wrong answers silently.
Production Insight
A payment routing engine used greedy for fee minimization, missing cheaper multi-hop paths and costing $200k/month.
Symptom: suboptimal routes passed all unit tests but failed in production under real fee structures.
Rule: If the problem has overlapping subproblems and optimal substructure, don't reach for greedy — reach for DP.
Key Takeaway
DP is not about recursion — it's about caching subproblem results to avoid redundant work.
Optimal substructure and overlapping subproblems are non-negotiable prerequisites for DP.
Start with brute force, then add memoization — never jump straight to a recurrence without understanding the subproblem graph.
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.
Coin Change 2 DP states (space-optimized) after each coin iteration
Step 1
1
1
1
1
1
1
Initial dp after coin=1 (all ones): dp[0]=1, dp[1]=1 ... dp[5]=1
Step 2
1
1
2
2
3
3
After coin=2: dp[2]=dp[2]+dp[0]=1+1=2; dp[4]=dp[4]+dp[2]=1+2=3
Step 3
1
1
2
2
3
4
After coin=5: dp[5]=dp[5]+dp[0]=3+1=4
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).
Space-Optimized LCS: Two rows (prev, cur) after each outer iteration
Step 1
0
0
0
0
0
0
Initial: prev row (all zeros) for i=0
Step 2
0
1
1
1
1
1
After i=1 (char 'a'): cur = [0,1,1,1,1,1]
Step 3
1
1
1
2
2
2
After i=2 (char 'b'): cur = [1,1,1,2,2,2] (prev becomes old cur)
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.
The Pathological Recurrence: Why Your State Definition Is the Only Thing That Matters
You've memorized the patterns. You've grinded 50 problems. Then a senior throws you a problem you've never seen, and your O(2^n) solution times out in the interview. The bottleneck isn't the algorithm — it's the state definition. Every DP problem is just a recurrence relation with a built-in dependency graph. The moment you define dp[i][j] as 'maximum profit from the first i items with capacity j', you've already won or lost. If your state is wrong, no optimization in the world saves you. The trick: trace the recurrence on paper before writing code. Find the smallest input that breaks your logic. Memoization is forgiving; tabulation punishes bad state definitions with a silent O(n^2) washout. In production, this kills more services than null pointers — an incorrect caching key that grows exponentially with input size. Define your state like your job depends on it. Because in an on-call rotation, it does.
StateDefinitionMatters.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
// io.thecodeforge// Bad: state explodes, O(2^n) on string lengthpublicintbadWildcardMatch(String s, String p) {
returndfs(s, p, 0, 0);
}
// Good: O(m*n) with clear 2D statepublicintisMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = newboolean[m+1][n+1];
dp[0][0] = true;
for (int j = 1; j <= n; j++) {
if (p.charAt(j-1) == '*') dp[0][j] = dp[0][j-2];
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p.charAt(j-1) == '.' || p.charAt(j-1) == s.charAt(i-1)) {
dp[i][j] = dp[i-1][j-1];
} elseif (p.charAt(j-1) == '*') {
dp[i][j] = dp[i][j-2];
if (p.charAt(j-2) == '.' || p.charAt(j-2) == s.charAt(i-1)) {
dp[i][j] = dp[i][j] || dp[i-1][j];
}
}
}
}
return dp[m][n] ? 1 : 0;
}
Output
isMatch("aab", "c*a*b") → 1
Production Trap:
Defining a state that includes the entire remaining string (like substring indices) instead of prefix lengths. Your cache key grows with input, turning a DP into an accidental brute force. Always reduce to the smallest unique identifier — usually prefix indices or a single bitmask.
Key Takeaway
State definition is the one line of code that determines whether your DP is O(n^2) or O(2^n). Get it wrong, and every optimization is just lipstick on a pig.
The Recurrence Relation Debugging Checklist: What to Do When Your DP Goes Silent
Your DP returns 0 for every input. No exceptions. No stack traces. Just wrong answers. This is the most common bug in production DP code — a recurrence that silently degenerates to the base case. The fix isn't more print statements. It's a three-point checklist. First, verify your base case returns something non-zero for the smallest valid input. Second, trace the recurrence manually for n=1 and n=2. Third, ensure every recursive call actually transforms the state — if your recurrence calls f(i, j) and both parameters stay identical, you've built an infinite loop that returns base case. In production, this manifests as a caching layer that always hits the null entry because the key never changes. The real debugging move: write the recurrence on paper, then translate it character-by-character to code. Every operator, every index shift. I've watched juniors spend three hours chasing a bug that was a single off-by-one in a state transition. The recurrence is the contract. If it's wrong, the code is a lie.
RecurrenceChecklist.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
// io.thecodeforgepublicintfibonacciBugged(int n) {
int[] dp = newint[n+1];
dp[0] = 0; dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2]; // correct
}
return dp[n];
}
// The silent killer: recurrence doesn't progresspublicintcountWays(int n) {
if (n <= 1) return1;
// BUG: dp[i] = dp[i-1] + dp[i-2]? No, it's just dp[i-1]
return countWays(n-1) + countWays(n-2); // innocent-looking but wrong
}
// Fixed: explicit state progressionpublicintcountWaysFixed(int n) {
int[] dp = newint[n+1];
dp[0] = 1; dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
Recurrence that references dp[i] instead of dp[i-1] inside the loop. The cache is populated out of order, returning stale 0s. Always print the first 5 values of your DP array after computation — if any are 0 where they shouldn't be, your recurrence is degenerate.
Key Takeaway
A recurrence that doesn't transform state is a silent infinite loop. Trace n=0, n=1, n=2 manually before trusting any DP code in production.
The Common Substring Threshold: Why LCS Variants Break in Production Without Compression
Longest Common Substring (not subsequence) looks like LCS but with a catch: you reset to 0 when characters don't match. That's easy. The production killer is when you need to find substrings above a length threshold — like 'find all common substrings longer than 3 characters between two DNA sequences'. The naive DP keeps the entire table, O(m*n) memory. With m=100k, n=100k, that's 10 billion entries. Your server crashes. The fix: sliding window DP with O(n) space. Only track the previous row, and store results in a hash set of start indices. But here's the trap: when you discard rows, you lose the ability to backtrack and reconstruct. If the interviewer asks 'print the longest substring', not just its length, you need a different strategy. Production compromise: store only the maximum length and the ending index in each row. Reconstruct by scanning the string at the end. This keeps memory O(n) and gives you the actual substring. I've seen this exact pattern at scale — a bioinformatics pipeline that ran out of memory every night at 3 AM. The root cause? A junior using 2D dp[][] for all-vs-all comparisons.
CommonSubstringOptimized.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
30
31
32
33
34
// io.thecodeforge// O(m*n) memory — don't use this for productionpublicintlcsNaive(String a, String b) {
int m = a.length(), n = b.length();
int[][] dp = newint[m+1][n+1];
int max = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (a.charAt(i-1) == b.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1] + 1;
max = Math.max(max, dp[i][j]);
}
}
}
return max;
}
// O(n) memory — production-readypublicintlcsOptimized(String a, String b) {
int m = a.length(), n = b.length();
int[] prev = newint[n+1];
int max = 0;
for (int i = 1; i <= m; i++) {
int[] curr = newint[n+1];
for (int j = 1; j <= n; j++) {
if (a.charAt(i-1) == b.charAt(j-1)) {
curr[j] = prev[j-1] + 1;
max = Math.max(max, curr[j]);
}
}
prev = curr;
}
return max;
}
Using O(m*n) DP for substring problems with large inputs (m,n > 10^4). The 2D array alone is 800 MB for 10k x 10k integers. One common mistake: thinking 'but I'll use short' — Java short is still 2 bytes per entry, 200 MB. Always compress to O(n) with two rolling arrays.
Key Takeaway
If your DP table size exceeds 10^6 entries, you need space optimization. For substring problems, two rolling arrays are the default — not the exception.
● 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.