Home Interview Top DP Interview Problems — Avoid StackOverflow in Prod
Advanced 11 min · March 06, 2026
Top 10 DP Interview Problems

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
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Deep production experience
  • Understanding of internals and trade-offs
  • Experience debugging complex systems
 ● 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.
top-dp-interview-problems DP Solution Architecture Layered components for building DP solutions Problem Layer Input Constraints | Objective Function | Decision Variables Pattern Layer 0/1 Knapsack | LCS | Unbounded Knapsack Algorithm Layer Memoization | Tabulation | Space Optimization Implementation Layer DP Table | Recurrence Relation | Base Cases THECODEFORGE.IO
thecodeforge.io
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
top-dp-interview-problems Memoization vs Tabulation Trade-offs between top-down and bottom-up DP approaches Memoization Tabulation Approach Top-down recursive with caching Bottom-up iterative table filling Space Complexity O(N) recursion stack + cache O(N) table (can optimize to O(1)) Ease of Implementation Easier for complex recurrences More control over state order Performance May have overhead from recursion Faster due to iterative loops Use Case When state space is sparse When all states are needed THECODEFORGE.IO
thecodeforge.io
Top Dp Interview Problems

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
Mental Model
Think of it as: 'solve substring, then glue'
Interval DP is like solving a puzzle by breaking it into smaller puzzles that cover contiguous segments.
  • 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.

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_patterns_framework.pyPYTHON
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
35
36
37
38
39
40
41
42
43
# 0/1 Knapsack
def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0]*(capacity+1) for _ in range(n+1)]
    for i in range(1, n+1):
        for w in range(1, capacity+1):
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
            else:
                dp[i][w] = dp[i-1][w]
    return dp[n][capacity]

# LCS
def lcs(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0]*(n+1) for _ in range(m+1)]
    for i in range(1, m+1):
        for j in range(1, n+1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = 1 + dp[i-1][j-1]
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    return dp[m][n]

# Palindromic Substrings count
def count_palindromic_substrings(s):
    n = len(s)
    dp = [[False]*n for _ in range(n)]
    count = 0
    for i in range(n-1, -1, -1):
        for j in range(i, n):
            if s[i]==s[j] and (j-i<2 or dp[i+1][j-1]):
                dp[i][j] = True
                count += 1
    return count

# Grid DP: Unique Paths
def unique_paths(m, n):
    dp = [[1]*n for _ in range(m)]
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i-1][j] + dp[i][j-1]
    return dp[m-1][n-1]
💡Pattern Recognition Shortcut
📊 Production Insight
In production, DP tables can be large. Always consider space optimization (e.g., rolling arrays) and use iterative bottom-up to avoid recursion stack overflow.
🎯 Key Takeaway
DP problems can be classified into a handful of patterns; recognizing the pattern is half the solution.

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.

decision_framework.pyPYTHON
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
# Greedy: Fractional Knapsack
def fractional_knapsack(weights, values, capacity):
    items = sorted(zip(weights, values), key=lambda x: x[1]/x[0], reverse=True)
    total_value = 0
    for w, v in items:
        if capacity >= w:
            total_value += v
            capacity -= w
        else:
            total_value += v * (capacity / w)
            break
    return total_value

# DP: 0/1 Knapsack (as above)
# Backtracking: Subset Sum (find all subsets)
def subset_sum_backtrack(nums, target):
    res = []
    def backtrack(start, path, total):
        if total == target:
            res.append(path[:])
            return
        if total > target:
            return
        for i in range(start, len(nums)):
            path.append(nums[i])
            backtrack(i+1, path, total+nums[i])
            path.pop()
    backtrack(0, [], 0)
    return res
🔥When Greedy Fails
📊 Production Insight
In production, greedy algorithms are preferred for their speed, but ensure correctness with formal proofs or extensive testing. DP is reliable but may require memory optimization.
🎯 Key Takeaway
Use Greedy for problems with greedy choice property, DP for overlapping subproblems, and Backtracking for exhaustive search on small inputs.

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.

Common Problems
  • Traveling Salesman Problem (TSP): dp[mask][i] = min cost to visit subset mask ending 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.

Implementation Tips
  • 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.

bitmask_dp.pyPYTHON
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
# TSP using bitmask DP
def tsp(dist):
    n = len(dist)
    dp = [[float('inf')]*n for _ in range(1<<n)]
    dp[1][0] = 0  # start at city 0
    for mask in range(1<<n):
        for u in range(n):
            if not (mask & (1<<u)):
                continue
            for v in range(n):
                if mask & (1<<v):
                    continue
                new_mask = mask | (1<<v)
                dp[new_mask][v] = min(dp[new_mask][v], dp[mask][u] + dist[u][v])
    full_mask = (1<<n)-1
    return min(dp[full_mask][i] + dist[i][0] for i in range(1, n))

# Subset Sum using bitmask (check existence)
def subset_sum_bitmask(nums, target):
    n = len(nums)
    for mask in range(1<<n):
        total = 0
        for i in range(n):
            if mask & (1<<i):
                total += nums[i]
        if total == target:
            return True
    return False
⚠ Bitmask Limitation
📊 Production Insight
In production, bitmask DP is rarely used due to scalability. However, for embedded systems or small-scale optimization (e.g., scheduling 10 tasks), it's a reliable exact method.
🎯 Key Takeaway
Bitmask DP enables solving subset problems with exponential states, ideal for small n (≤20) and problems like TSP or subset sum.
● 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)
⚙ Quick Reference
17 commands from this guide
FileCommand / CodePurpose
iothecodeforgedpPatternClassifier.javapublic class PatternClassifier {DP Pattern Matrix for Rapid Classification
iothecodeforgedpCoinChange2TableWalkthrough.javapublic class CoinChange2TableWalkthrough {Visual State Transition Table Walkthrough
iothecodeforgedpFibonacciComparison.javapublic class FibonacciComparison {Memoization vs Tabulation Comparison Matrix
iothecodeforgedpLcsSpaceOptimized.javapublic class LcsSpaceOptimized {Space Optimization
iothecodeforgedpKnapsackSolver.javapublic class KnapsackSolver {Core Pattern
iothecodeforgedpLcsService.javapublic class LcsService {Sequence Pattern
iothecodeforgedpCoinChangeSolver.javapublic class CoinChangeSolver {Unbounded Knapsack
iothecodeforgedpMatrixChainMultiplication.javapublic class MatrixChainMultiplication {Interval DP
iothecodeforgedpUniquePaths.javapublic class UniquePaths {Grid DP
iothecodeforgedpLIS.javapublic class LIS {Longest Increasing Subsequence (LIS) – The O(n log n) Varian
iothecodeforgedpEditDistance.javapublic class EditDistance {Edit Distance (Levenshtein Distance) – String Alignment DP
StateDefinitionMatters.javapublic int badWildcardMatch(String s, String p) {The Pathological Recurrence
RecurrenceChecklist.javapublic int fibonacciBugged(int n) {The Recurrence Relation Debugging Checklist
CommonSubstringOptimized.javapublic int lcsNaive(String a, String b) {The Common Substring Threshold
dp_patterns_framework.pydef knapsack(weights, values, capacity):DP Patterns Framework
decision_framework.pydef fractional_knapsack(weights, values, capacity):DP vs Greedy vs Backtracking
bitmask_dp.pydef tsp(dist):DP with Bitmask for Subset Problems

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.
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
July 19, 2026
last updated
2,466
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 / 26 · Coding Patterns
Next
Top 10 Graph Interview Problems