Senior 11 min · March 06, 2026

Top DP Interview Problems — Avoid StackOverflow in Prod

Recursive DP caused StackOverflowError after 5000 calls in production.

N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
DP Interview Problems: Pattern & Optimization Guide THECODEFORGE.IO DP Interview Problems: Pattern & Optimization Guide From classification to space-optimized solutions for production DP Pattern Matrix Classify problem: 0/1 Knapsack, LCS, Unbounded State Transition Table Visualize recurrence: dp[i][j] = max(...) Memoization vs Tabulation Top-down recursion vs bottom-up iteration Space Optimization Reduce O(N²) to O(N) using rolling arrays 0/1 Knapsack Framework Core pattern: include/exclude items Unbounded Knapsack Coin Change & Rod Cutting: unlimited repeats ⚠ Forgetting to initialize dp[0] correctly Always set base case: dp[0][*] = 0 or INF as needed THECODEFORGE.IO
thecodeforge.io
DP Interview Problems: Pattern & Optimization Guide
Top Dp Interview Problems

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.

PatternState DefinitionTransition ShapeTypical Base Case
0/1 Knapsackdp[i][w] = max value using first i items, capacity wdp[i][w] = max(dp[i-1][w], val[i] + dp[i-1][w-wt[i]])dp[0][]=0, dp[][0]=0
Unbounded Knapsackdp[w] = min/max value for capacity w (item index not needed)dp[w] = min(dp[w], dp[w-coin] + 1) forward loopdp[0]=0, rest = INF
Sequence (LCS/Edit Distance)dp[i][j] = answer for prefix i of string1, prefix j of string2if match: diag+1; else: max(left,up) or min of three editsdp[0][]= , dp[][0]=
Interval DPdp[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 DPdp[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 lbinary search tails to place xtails[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.

io/thecodeforge/dp/PatternClassifier.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Quick classification snippet — not real code
public class PatternClassifier {
    public String classify(String problemDescription) {
        if (problemDescription.contains("substring") || problemDescription.contains("range")) {
            return "INTERVAL_DP";
        } else if (problemDescription.contains("grid") || problemDescription.contains("matrix")) {
            return "GRID_DP";
        } else if (problemDescription.contains("capacity") || problemDescription.contains("weight")) {
            return "KNAPSACK";
        } else if (problemDescription.contains("sequence") || problemDescription.contains("subsequence")) {
            return "SEQUENCE_DP";
        } else if (problemDescription.contains("increasing")) {
            return "LIS";
        } else {
            return "ANALYZE_MORE";
        }
    }
}
Output
(No output — snippet illustrative)
Forge Tip: Keep this matrix handy
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.

io/thecodeforge/dp/CoinChange2TableWalkthrough.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
public class CoinChange2TableWalkthrough {
    public int change(int amount, int[] coins) {
        int[] dp = new int[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.

DimensionMemoization (Top-Down)Tabulation (Bottom-Up)
Ease of implementationEasier if you already have a recursive solutionRequires iterative loops; risk of index off-by-one
Time complexitySame O(state space * transition cost) but function call overheadUsually faster due to cache-friendly loops
Space complexityO(states) for memo map + call stack O(depth) – may be O(N) extraTypically uses explicit DP array – can be optimized to O(1) or O(N)
Stack safetyRisk of StackOverflow for large depth (N > 1000)No recursion, always stack safe
Pruning efficiencyNaturally prunes unreachable states – only computes needed statesAlways iterates over full state space – can be wasteful
Optimal substructure clarityVery clear: recursion mirrors the recurrence directlyOften hidden in loops
Space optimizationHard 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
public class FibonacciComparison {
    // Memoization
    static int[] memo;
    public static int fibMemo(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];
    }

    // Tabulation
    public static int fibTab(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;
    }

    public static void main(String[] args) {
        int n = 100;
        memo = new int[n+1];
        System.out.println("Memo: " + fibMemo(n));  // could stack overflow for n=10000
        System.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
public class LcsSpaceOptimized {
    // O(m*n) time, O(min(m,n)) space
    public int lcsOptimized(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 row

        for (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 rows
            int[] temp = prev;
            prev = cur;
            cur = temp;
            // clear cur for next iteration (optional, as values will be overwritten)
            Arrays.fill(cur, 0);
        }
        return prev[n];
    }
}
Output
// Example: text1="abcde", text2="ace" -> Output: 3
Memory Savings Add Up
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;

public class KnapsackSolver {
    /**
     * Standard 0/1 Knapsack Implementation
     * Time Complexity: O(N * W), Space Complexity: O(W)
     */
    public int solveKnapsack(int[] values, int[] weights, int capacity) {
        int n = values.length;
        // Space optimized 1D DP array
        int[] dp = new int[capacity + 1];

        for (int i = 0; i < n; i++) {
            // Iterate backwards to ensure we don't use the same item twice
            for (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;

public class LcsService {
    /**
     * io.thecodeforge: Standard LCS implementation
     * Handles the core of problems like 'Edit Distance' and 'Longest Palindromic Subsequence'
     */
    public int findLCS(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[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;

public class CoinChangeSolver {
    /**
     * Minimum number of coins to make amount.
     * Unbounded knapsack – iterate forward.
     */
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[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];
    }
}
Output
// coins = [1,2,5], amount = 11 -> Output: 3 (5+5+1)
The Direction Matters
Forward loop = unbounded (unlimited supply). Backward loop = 0/1 (each used once). This is the key difference testers check when you write the code.
Production Insight
Coin Change with large amounts (10^4) and many coins (500) is fine for O(N*amount).
But if amount is 10^6, the DP array becomes 4MB – okay, but the O(N*amount) time might be borderline. Use BFS (shortest path) as an alternative.
Common bug: initializing dp with Integer.MAX_VALUE causes overflow when adding 1. Use amount+1 sentinel instead.
Never forget to check dp[amount] > amount -> return -1.
Key Takeaway
Unbounded knapsack loops forward to allow repeat usage.
Coin Change = min coins, dp[x] = min(dp[x], 1+dp[x-coin]).
Coin Change 2 = number of combinations – same pattern but dp[x] += dp[x-coin] and outer loop over coins.

Interval DP: Matrix Chain Multiplication & Palindromic Substrings

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.

io/thecodeforge/dp/MatrixChainMultiplication.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
package io.thecodeforge.dp;

public class MatrixChainMultiplication {
    /**
     * Compute minimum multiplication cost for matrices A1A2...An
     */
    public int minCost(int[] dimensions) {
        int n = dimensions.length - 1; // number of matrices
        int[][] dp = new int[n][n];

        // length L from 2 to n
        for (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;

public class UniquePaths {
    /**
     * Robot from top-left to bottom-right, only down or right.
     */
    public int uniquePaths(int m, int n) {
        int[] dp = new int[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.
Space optimized: 1D array, dp[j] = dp[j] (above) + dp[j-1] (left).
Variations: obstacles (reset to 0), costs (take min), two-way traversal (Dungeon Game).

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;

public class LIS {
    public int lengthOfLIS(int[] nums) {
        int[] tails = new int[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 (Levenshtein Distance) – String Alignment DP

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;

public class EditDistance {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        int[][] dp = new int[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],    // delete
                        Math.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 length
public int badWildcardMatch(String s, String p) {
    return dfs(s, p, 0, 0);
}

// Good: O(m*n) with clear 2D state
public int isMatch(String s, String p) {
    int m = s.length(), n = p.length();
    boolean[][] dp = new boolean[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];
            } else if (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.thecodeforge
public int fibonacciBugged(int n) {
    int[] dp = new int[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 progress
public int countWays(int n) {
    if (n <= 1) return 1;
    // 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 progression
public int countWaysFixed(int n) {
    int[] dp = new int[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];
}
Output
countWays(5) → 8 (correct), countWaysBugged(5) → 6 (silent failure)
Production Trap:
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 production
public int lcsNaive(String a, String b) {
    int m = a.length(), n = b.length();
    int[][] dp = new int[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-ready
public int lcsOptimized(String a, String b) {
    int m = a.length(), n = b.length();
    int[] prev = new int[n+1];
    int max = 0;
    for (int i = 1; i <= m; i++) {
        int[] curr = new int[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;
}
Output
lcsOptimized("ababc", "babca") → 3 (substring "bab")
Production Trap:
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 CategoryCommon ProblemsCore Transition Logic
0/1 KnapsackSubset Sum, Target Sum, Partition Equal Subset SumExclude vs. Include (max(dp[i-1][w], val + dp[i-1][w-wt]))
Unbounded KnapsackCoin Change, Coin Change 2, Rod CuttingInclude multiple times (forward loop: dp[w] = min(dp[w], 1+dp[w-wt]))
Sequence DP (LCS)Edit Distance, Longest Palindromic Subsequence, Longest Common SubstringMatch (1+dp[i-1][j-1]) vs Mismatch (max of dp[i-1][j], dp[i][j-1])
Interval DPMatrix Chain Multiplication, Palindromic Substrings, Burst BalloonsSplit into [i,k] and [k+1,j], combine cost
Grid DPUnique Paths, Minimum Path Sum, Dungeon GameFrom above + left (dp[i][j] = f(dp[i-1][j], dp[i][j-1]))
LIS (Patience Sorting)Longest Increasing Subsequence, Russian Doll Envelopes, MSTBinary search tail array to find position to place current element
Edit DistanceLevenshtein Distance, One Edit Distance, Minimum cost to make strings equalMatch (no cost) vs Insert/Delete/Replace (1 + min of adjacent states)

Key takeaways

1
DP is just recursion + memoization or bottom-up iteration. The patterns repeat.
2
You've seen the five core patterns
0/1 Knapsack, LCS, Unbounded Knapsack, Interval DP, Grid DP, LIS (patience), and Edit Distance.
3
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; } ``
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
When should I use Top-Down vs. Bottom-Up DP?
02
How do I recognize a problem can be solved with Dynamic Programming?
03
What is the difference between 0/1 Knapsack and Unbounded Knapsack?
04
How do I optimize space in DP from 2D to 1D?
N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Coding Patterns. Mark it forged?

11 min read · try the examples if you haven't

Previous
Top 10 Tree Interview Problems
4 / 17 · Coding Patterns
Next
Top 10 Graph Interview Problems