Top DP Interview Problems — Avoid StackOverflow in Prod
Recursive DP caused StackOverflowError after 5000 calls in production.
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- 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.
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.
DP Pattern Matrix for Rapid Classification
When you see a new problem, ask yourself three questions: What is the state? What is the recurrence shape? What are the base cases? These form a pattern matrix that maps every DP problem to one of five archetypes. Use the table below to classify within seconds.
| Pattern | State Definition | Transition Shape | Typical Base Case |
|---|---|---|---|
| 0/1 Knapsack | dp[i][w] = max value using first i items, capacity w | dp[i][w] = max(dp[i-1][w], val[i] + dp[i-1][w-wt[i]]) | dp[0][]=0, dp[][0]=0 |
| Unbounded Knapsack | 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.
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.
- 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.
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.
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.
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.'
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.
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.
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.
- 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).
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.
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'.
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.
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.
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.
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.
DP Patterns Framework: 0/1 Knapsack, LCS, Palindromes, DP on Grid
Mastering DP requires recognizing patterns. This section unifies four core patterns: 0/1 Knapsack, Longest Common Subsequence (LCS), Palindromic Substrings, and DP on Grids. Each pattern has a characteristic state definition and recurrence.
0/1 Knapsack: Given weights and values, maximize value without exceeding capacity. State: dp[i][w] = max value using first i items with capacity w. Recurrence: dp[i][w] = max(dp[i-1][w], dp[i-1][w-wi] + vi). Example: Subset Sum, Partition Equal Subset Sum.
LCS: Find longest subsequence common to two strings. State: dp[i][j] = length of LCS of prefixes of length i and j. Recurrence: if s1[i-1]==s2[j-1] then dp[i][j]=1+dp[i-1][j-1] else dp[i][j]=max(dp[i-1][j], dp[i][j-1]). Example: Edit Distance, Shortest Common Supersequence.
Palindromic Substrings: Count or find longest palindromic substring. State: dp[i][j] = true if substring i..j is palindrome. Recurrence: dp[i][j] = (s[i]==s[j] and (j-i<2 or dp[i+1][j-1])). Example: Palindromic Partitioning, Longest Palindromic Subsequence.
DP on Grid: Count paths or find min cost in a grid. State: dp[i][j] = number of ways or min cost to reach (i,j). Recurrence: dp[i][j] = dp[i-1][j] + dp[i][j-1] (for unique paths) or dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) (for min path sum). Example: Unique Paths II, Minimum Path Sum.
These patterns share a common structure: define state, write recurrence, handle base cases, and iterate. Recognizing which pattern applies is the key to solving DP problems quickly.
DP vs Greedy vs Backtracking: Decision Framework
Choosing the right algorithmic paradigm is critical. This framework helps decide between DP, Greedy, and Backtracking based on problem properties.
Greedy: Use when a local optimum leads to global optimum. Properties: optimal substructure (without overlapping subproblems) and greedy choice property. Example: Fractional Knapsack, Dijkstra's algorithm. If you can prove that making the best choice at each step yields the optimal solution, go greedy. It's O(n log n) or O(n).
Backtracking: Use when you need to explore all possibilities (exhaustive search) and the solution space is small. It's essentially brute force with pruning. Example: N-Queens, Sudoku solver. Time complexity is exponential, so only feasible for small inputs (n ≤ 20).
DP: Use when there are overlapping subproblems and optimal substructure. DP caches results to avoid recomputation. Example: 0/1 Knapsack, Edit Distance. Time complexity is polynomial (O(n^2) or O(n*capacity)).
Decision Flowchart: 1. Does the problem ask for a single optimal solution (max/min) or count? → DP or Greedy. 2. Can you make a decision that never needs to be undone? → Greedy. 3. Are there overlapping subproblems? → DP. 4. Is the input size small and need all solutions? → Backtracking.
Example: Coin Change (minimum coins). Greedy fails for arbitrary denominations (e.g., coins [1,3,4], amount 6: greedy gives 4+1+1=3 coins, but optimal is 3+3=2). DP works. But for canonical coin systems (e.g., US coins), greedy works.
Production Note: Greedy is fastest, DP is moderate, backtracking is slowest. Always profile test cases to ensure correctness.
DP with Bitmask for Subset Problems
Bitmask DP is a powerful technique for problems involving subsets, especially when the number of elements is small (n ≤ 20). It uses an integer's bits to represent a set: bit i is 1 if element i is included.
State: dp[mask] = optimal value for the subset represented by mask. Often combined with a second dimension (e.g., last element) for path problems.
- Traveling Salesman Problem (TSP):
dp[mask][i]= min cost to visit subsetmaskending at city i. Recurrence:dp[mask][i] = min(dp[mask ^ (1< for j in mask. - Partition Equal Subset Sum:
dp[mask]= sum of subset. Check if any subset sums to target. - Minimum Cost to Assign Tasks:
dp[mask]= min cost to complete tasks in mask.
Example: Given n items with weights, find if there exists a subset with sum exactly S. Use bitmask to iterate all subsets: O(2^n). For n=20, 2^20 ≈ 1e6, feasible.
- Use 0-indexed bits.
- Iterate masks from 0 to (1<
- For each mask, iterate over bits to transition.
- Precompute costs or distances if needed.
Production Note: Bitmask DP is exponential, so only use for small n. For larger n, consider meet-in-the-middle or approximation algorithms.
The Recursive DP That Took Down a Production Scheduler
- 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.
System.out.println("state: " + i + ", " + j);Verify memoization map is being used – ensure you store result before returning.| File | Command / Code | Purpose |
|---|---|---|
| io | public class PatternClassifier { | DP Pattern Matrix for Rapid Classification |
| io | public class CoinChange2TableWalkthrough { | Visual State Transition Table Walkthrough |
| io | public class FibonacciComparison { | Memoization vs Tabulation Comparison Matrix |
| io | public class LcsSpaceOptimized { | Space Optimization |
| io | public class KnapsackSolver { | Core Pattern |
| io | public class LcsService { | Sequence Pattern |
| io | public class CoinChangeSolver { | Unbounded Knapsack |
| io | public class MatrixChainMultiplication { | Interval DP |
| io | public class UniquePaths { | Grid DP |
| io | public class LIS { | Longest Increasing Subsequence (LIS) – The O(n log n) Varian |
| io | public class EditDistance { | Edit Distance (Levenshtein Distance) – String Alignment DP |
| StateDefinitionMatters.java | public int badWildcardMatch(String s, String p) { | The Pathological Recurrence |
| RecurrenceChecklist.java | public int fibonacciBugged(int n) { | The Recurrence Relation Debugging Checklist |
| CommonSubstringOptimized.java | public int lcsNaive(String a, String b) { | The Common Substring Threshold |
| dp_patterns_framework.py | def knapsack(weights, values, capacity): | DP Patterns Framework |
| decision_framework.py | def fractional_knapsack(weights, values, capacity): | DP vs Greedy vs Backtracking |
| bitmask_dp.py | def tsp(dist): | DP with Bitmask for Subset Problems |
Key takeaways
Interview Questions on This Topic
Given an array of integers, find the maximum sum of a non-adjacent subsequence. (House Robber variant). Walk through the recurrence and optimize space.
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;
}
``Frequently Asked Questions
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
That's Coding Patterns. Mark it forged?
11 min read · try the examples if you haven't