Loading learning content...
You've identified a DP problem, designed your state carefully, and implemented a solution. It compiles, it runs... and it gives the wrong answer. Welcome to the most frustrating part of Dynamic Programming.
DP bugs are notoriously difficult to find because the problems are inherently complex, the code often involves nested loops with subtle index manipulations, and small errors propagate through the entire computation. A single off-by-one error can corrupt every cell in your DP table.
This page provides systematic techniques for debugging DP solutions. By the end, you'll have a toolkit of strategies that transforms debugging from 'stare at code and hope' into a methodical process.
By completing this page, you will be able to:
• Recognize and fix the most common categories of DP bugs • Apply systematic debugging techniques specific to DP problems • Use table visualization to understand solution behavior • Develop testing strategies that catch DP errors before submission • Build defensive coding habits that prevent bugs in the first place
DP bugs fall into predictable categories. Recognizing these categories accelerates diagnosis—you know what to look for before you even start debugging.
| Bug Category | Common Symptoms | Where to Look |
|---|---|---|
| Off-by-one | Wrong for edge cases, off by 1 from expected | Loop bounds, array indices, base case values |
| Base case | Wrong for small inputs, NaN/undefined appears | Initialization code, boundary conditions |
| Transition logic | Consistently wrong by a pattern | The main recurrence formula |
| Iteration order | Wrong values propagate, partial correct regions | Loop construction, dependency analysis |
| State design | Fundamentally wrong, no simple fix visible | State definition itself |
| Index convention | Off-by-one but inconsistently | Index usage throughout code |
| Overflow | Negative numbers, unexpectedly small results | Accumulation operations, large inputs |
| Memoization | Correct but slow, or wrong for repeated calls | Cache key construction, cache checks |
In practice, about 80% of DP bugs fall into three categories: off-by-one errors, base case mistakes, and iteration order problems. These should be your first suspects. Transition logic errors are typically caught during design/code review, while state design flaws indicate fundamental misunderstanding of the problem.
Off-by-one errors (OBOEs) are the most common DP bugs, and they're insidious because they often produce results that are 'almost right'—just 1 off from the correct answer. This makes them easy to dismiss as test case issues rather than code bugs.
for (i = 0; i < n-1; i++) should be i < nfor (i = 0; i <= n; i++) when array has n elements (0 to n-1)new Array(n) when you need indices 0 to n, requiring size n+1dp[n-1] when answer is at dp[n] (or vice versa)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// PROBLEM: Climbing Stairs - count ways to reach step n // ❌ BUG: Array too smallfunction climbWrong1(n: number): number { if (n <= 1) return 1; const dp = new Array(n); // Has indices 0 to n-1 dp[0] = 1; dp[1] = 1; for (let i = 2; i <= n; i++) { // Tries to access dp[n] dp[i] = dp[i-1] + dp[i-2]; // CRASH or undefined at i=n } return dp[n]; // Out of bounds!} // ✓ FIXED: Array size n+1function climbCorrect1(n: number): number { if (n <= 1) return 1; const dp = new Array(n + 1); // Has indices 0 to n dp[0] = 1; dp[1] = 1; for (let i = 2; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; // Correct!} // ❌ BUG: Loop ends too earlyfunction climbWrong2(n: number): number { if (n <= 1) return 1; const dp = new Array(n + 1); dp[0] = 1; dp[1] = 1; for (let i = 2; i < n; i++) { // Should be i <= n dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; // dp[n] never computed, returns undefined!} // ✓ FIXED: Loop includes nfunction climbCorrect2(n: number): number { if (n <= 1) return 1; const dp = new Array(n + 1); dp[0] = 1; dp[1] = 1; for (let i = 2; i <= n; i++) { // Correct bound dp[i] = dp[i-1] + dp[i-2]; } return dp[n];}The most dangerous OBOE comes from confusion about inclusive vs exclusive bounds.
• 'from i to j' — does this include j? • 'first n elements' — is this indices 0 to n-1, or 1 to n? • dp[i] represents... what exactly?
Document your convention explicitly and verify consistency throughout your code.
Base case errors are particularly dangerous because they corrupt the entire computation from the foundation up. A wrong base case means every derived value is wrong—even if your transition logic is perfect.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// PROBLEM: Coin Change - minimum coins to make amount // ❌ BUG: Wrong initializationfunction coinChangeWrong(coins: number[], amount: number): number { const dp = new Array(amount + 1).fill(0); // Should be Infinity! dp[0] = 0; // This is correct for (let a = 1; a <= amount; a++) { for (const coin of coins) { if (coin <= a) { dp[a] = Math.min(dp[a], 1 + dp[a - coin]); // min(0, 1 + something) is always ≤ 1, giving wrong answers } } } return dp[amount];} // ✓ FIXED: Initialize with Infinityfunction coinChangeCorrect(coins: number[], amount: number): number { const dp = new Array(amount + 1).fill(Infinity); // Correct! dp[0] = 0; for (let a = 1; a <= amount; a++) { for (const coin of coins) { if (coin <= a) { dp[a] = Math.min(dp[a], 1 + dp[a - coin]); } } } return dp[amount] === Infinity ? -1 : dp[amount];} // PROBLEM: Longest Increasing Subsequence // ❌ BUG: Missing base case valuefunction lisWrong(nums: number[]): number { const n = nums.length; const dp = new Array(n); // undefined values! for (let i = 0; i < n; i++) { for (let j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); // dp[i] is undefined, Math.max returns NaN } } } return Math.max(...dp);} // ✓ FIXED: Initialize base case (each element is LIS of length 1)function lisCorrect(nums: number[]): number { const n = nums.length; const dp = new Array(n).fill(1); // Base: each element alone is LIS of 1 for (let i = 0; i < n; i++) { for (let j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } } return Math.max(...dp);}Base Case Verification Checklist:
Bottom-up DP requires filling the table in an order that respects dependencies—each cell must be computed after all cells it depends on. Getting this wrong produces subtle bugs where some cells are correct but others use uninitialized values.
123456789101112131415161718192021222324252627282930313233343536373839404142
// PROBLEM: Longest Common Subsequence// dp[i][j] depends on dp[i-1][j], dp[i][j-1], and dp[i-1][j-1] // ❌ BUG: Wrong iteration directionfunction lcsWrong(s1: string, s2: string): number { const m = s1.length, n = s2.length; const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); // Iterating backwards, but our dp[i][j] uses i-1 and j-1 // which are LATER in this order - not yet computed! for (let i = m; i >= 1; i--) { for (let j = n; j >= 1; j--) { if (s1[i-1] === s2[j-1]) { dp[i][j] = 1 + dp[i-1][j-1]; // dp[i-1][j-1] is 0 (uncomputed) } else { dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); } } } return dp[m][n];} // ✓ FIXED: Correct iteration directionfunction lcsCorrect(s1: string, s2: string): number { const m = s1.length, n = s2.length; const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); // Iterating forwards, so dp[i-1][j-1], dp[i-1][j], dp[i][j-1] // are all computed before we need them for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (s1[i-1] === s2[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];}| Problem Type | Dependencies | Correct Order |
|---|---|---|
| Fibonacci-like | dp[i] ← dp[i-1], dp[i-2] | i = 2 to n (increasing) |
| LCS / Edit Distance | dp[i][j] ← dp[i-1][j], dp[i][j-1], dp[i-1][j-1] | i, j both increasing |
| Knapsack | dp[i][w] ← dp[i-1][w], dp[i-1][w-weight] | i increasing, w any order (or increasing) |
| Interval DP | dp[l][r] ← dp[l][k], dp[k+1][r] | By length: len = 1 to n, then all (l, r) with that length |
| LIS (one approach) | dp[i] ← max(dp[j]) for j < i | i = 0 to n-1 (increasing) |
If you're struggling with iteration order, top-down memoization automatically handles dependencies for you. Get the solution working with memoization first, then convert to bottom-up if needed for performance.
One of the most powerful debugging techniques for DP is visualizing the DP table. Seeing the actual computed values often makes bugs immediately obvious—incorrect values stand out, patterns in errors reveal systematic issues, and the flow of computation becomes visible.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// UTILITY: Print a 2D DP table for debuggingfunction printDPTable( dp: number[][], rowLabels?: string[], colLabels?: string[], title: string = "DP Table"): void { console.log(`=== ${title} ===`); // Header row with column labels const colHeader = colLabels ? " " + colLabels.map(l => l.padStart(5)).join(" ") : " " + dp[0].map((_, j) => j.toString().padStart(5)).join(" "); console.log(colHeader); // Data rows with row labels for (let i = 0; i < dp.length; i++) { const rowLabel = (rowLabels?.[i] ?? i.toString()).padStart(3); const rowData = dp[i] .map(v => { if (v === Infinity) return " ∞ "; if (v === -Infinity) return " -∞ "; if (v === undefined) return " ? "; return v.toString().padStart(5); }) .join(" "); console.log(`${rowLabel}: ${rowData}`); }} // EXAMPLE USAGE: LCS of "ABCD" and "AEBD"function lcsWithVisualization(s1: string, s2: string): number { const m = s1.length, n = s2.length; const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (s1[i-1] === s2[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]); } } } // Visualize const rowLabels = ["ε", ...s1.split("")]; // ε for empty prefix const colLabels = ["ε", ...s2.split("")]; printDPTable(dp, rowLabels, colLabels, "LCS Table"); return dp[m][n];} // OUTPUT:// === LCS Table ===// ε A E B D// ε: 0 0 0 0 0// A: 0 1 1 1 1// B: 0 1 1 2 2// C: 0 1 1 2 2// D: 0 1 1 2 3Before looking at your code's table, manually compute the expected table for a small example. Then compare. Differences pinpoint exactly which cells are wrong, narrowing down the bug location.
When your DP solution is wrong, follow this systematic process instead of randomly changing things until it works:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// SCENARIO: House Robber returns wrong answer// nums = [2, 7, 9, 3, 1], expected = 12, got = 11 // STEP 1: State definition// dp[i] = "max money from robbing houses 0 to i"// Hmm, is this right? What does dp[4] represent? // STEP 2: Base cases// dp[0] = nums[0] = 2 ✓// dp[1] = max(nums[0], nums[1]) = 7 ✓ // STEP 3: Transitions// dp[i] = max(dp[i-1], dp[i-2] + nums[i])// Let's trace: dp[2] = max(7, 2 + 9) = 11 ✓// dp[3] = max(11, 7 + 3) = 11... wait // INSIGHT: Something's wrong. Let's compute manually:// Rob houses 0, 2, 4: 2 + 9 + 1 = 12// Rob houses 1, 3: 7 + 3 = 10// Rob house 1, then 4: 7 + 1 = 8... but can we do 0, 2, 4? // Let me trace dp[4]:// dp[4] = max(dp[3], dp[2] + nums[4]) = max(11, 11 + 1) = 12 // Hmm, that's correct. Let me check my actual code...// *looks at code* function robBuggy(nums: number[]): number { const n = nums.length; if (n === 1) return nums[0]; const dp = new Array(n); dp[0] = nums[0]; dp[1] = Math.max(nums[0], nums[1]); for (let i = 2; i < n - 1; i++) { // BUG FOUND: i < n-1 should be i < n dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]); } return dp[n-1]; // This accesses uncomputed cell!} // STEP 6: Loop bounds check revealed the bug!// Fixed:function robFixed(nums: number[]): number { const n = nums.length; if (n === 1) return nums[0]; const dp = new Array(n); dp[0] = nums[0]; dp[1] = Math.max(nums[0], nums[1]); for (let i = 2; i < n; i++) { // Fixed bound dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]); } return dp[n-1];}Resist the urge to randomly change +1, -1, <, <=, until tests pass. You might 'fix' the bug by canceling errors, creating fragile code that breaks on other inputs. Understand WHY before changing.
Effective testing catches DP bugs before they become debugging sessions. Here are strategies tailored to DP's unique characteristics:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
// STRATEGY: Compare DP against brute force for small inputs // BRUTE FORCE: Knapsack via subset enumerationfunction knapsackBruteForce( weights: number[], values: number[], capacity: number): number { const n = weights.length; let maxValue = 0; // Enumerate all 2^n subsets for (let mask = 0; mask < (1 << n); mask++) { let totalWeight = 0; let totalValue = 0; for (let i = 0; i < n; i++) { if (mask & (1 << i)) { totalWeight += weights[i]; totalValue += values[i]; } } if (totalWeight <= capacity) { maxValue = Math.max(maxValue, totalValue); } } return maxValue;} // DP SOLUTION: The one we're testingfunction knapsackDP( weights: number[], values: number[], capacity: number): number { const n = weights.length; const dp: number[][] = Array.from({ length: n + 1 }, () => new Array(capacity + 1).fill(0)); for (let i = 1; i <= n; i++) { for (let w = 0; w <= capacity; w++) { dp[i][w] = dp[i-1][w]; // Don't take if (weights[i-1] <= w) { dp[i][w] = Math.max( dp[i][w], dp[i-1][w - weights[i-1]] + values[i-1] ); } } } return dp[n][capacity];} // TEST HARNESSfunction testKnapsack(): void { const testCases = [ // Boundary cases { w: [], v: [], c: 0 }, { w: [1], v: [1], c: 0 }, { w: [1], v: [1], c: 1 }, // Known cases { w: [1, 2, 3], v: [6, 10, 12], c: 5 }, // Random cases ...Array.from({ length: 10 }, () => ({ w: Array.from({ length: 8 }, () => Math.floor(Math.random() * 10) + 1), v: Array.from({ length: 8 }, () => Math.floor(Math.random() * 20) + 1), c: Math.floor(Math.random() * 30) + 1 })) ]; let passed = 0; for (const { w, v, c } of testCases) { const expected = knapsackBruteForce(w, v, c); const actual = knapsackDP(w, v, c); if (expected === actual) { passed++; } else { console.log(`FAIL: weights=${w}, values=${v}, capacity=${c}`); console.log(` Expected: ${expected}, Got: ${actual}`); } } console.log(`Passed ${passed}/${testCases.length} tests`);}For most DP problems, you can write a simple (but slow) brute force solution. Use this as your 'oracle'—the source of truth for testing. If brute force and DP disagree on small inputs, your DP has a bug.
The best debugging is the debugging you don't have to do. These coding habits prevent bugs before they happen:
currentRow is clearer than i; remainingCapacity better than w.1234567891011121314151617181920212223242526272829303132333435363738394041424344
function solveWithDefensiveHabits(input: InputType): ResultType { const n = input.length; // ======================================== // STATE DEFINITION: // dp[i] = "maximum score achievable from index i to end" // // TRANSITION: // dp[i] = max(choice1Outcome, choice2Outcome) // = max(dp[i+1], input[i] + dp[i+2]) // // BASE CASES: // dp[n] = 0 (no elements left) // dp[n-1] = input[n-1] (only one element) // ======================================== // EDGE CASES if (n === 0) return 0; // Empty input if (n === 1) return input[0]; // Single element // ALLOCATE: n+1 because we need dp[n] as base case const dp = new Array(n + 1); // INITIALIZE BASE CASES dp[n] = 0; dp[n - 1] = input[n - 1]; // FILL TABLE (right to left for this problem) for (let i = n - 2; i >= 0; i--) { // INVARIANT: dp[i+1] and dp[i+2] are already computed console.assert( dp[i + 1] !== undefined && dp[i + 2] !== undefined, `Dependencies not ready at i=${i}` ); dp[i] = Math.max( dp[i + 1], // Skip current input[i] + dp[i + 2] // Take current, skip next ); } // ANSWER: From the start return dp[0];}The time spent writing a clear state definition comment is repaid many times over when you don't have to debug a subtle bug. Future you (and your teammates) will thank present you.
Some DP bugs are more subtle than off-by-one errors. Here are techniques for harder debugging scenarios:
12345678910111213141516171819202122232425262728293031323334353637383940
// SCENARIO: DP solution fails for nums = [3,1,5,8,2,9,4,6,7,0]// How to find the minimal failing case? function binarySearchFailingCase(nums: number[]): number[] { // Does it still fail with half the input? const mid = Math.floor(nums.length / 2); const leftHalf = nums.slice(0, mid); const rightHalf = nums.slice(mid); // Test each half const expectedLeft = bruteForce(leftHalf); const actualLeft = dp(leftHalf); const leftFails = expectedLeft !== actualLeft; const expectedRight = bruteForce(rightHalf); const actualRight = dp(rightHalf); const rightFails = expectedRight !== actualRight; if (leftFails && leftHalf.length > 1) { return binarySearchFailingCase(leftHalf); // Recurse on failing half } else if (rightFails && rightHalf.length > 1) { return binarySearchFailingCase(rightHalf); } else { // Neither half fails alone, but together they fail // The bug might be at the boundary // Try progressively expanding from middle for (let size = 2; size <= nums.length; size++) { for (let start = 0; start + size <= nums.length; start++) { const subset = nums.slice(start, start + size); if (bruteForce(subset) !== dp(subset)) { return subset; // Found minimal failing case! } } } return nums; // Can't reduce further }} // This gives you a minimal failing case to analyze in detailSometimes the bug is so fundamental that patching is futile. If you've spent 30+ minutes debugging without progress, consider:
A clean rewrite often takes less time than extended debugging.
Debugging DP solutions is challenging but learnable. With the right techniques, you can systematically find and fix bugs instead of struggling in frustration. Let's consolidate the key insights:
The DP Problem-Solving Framework Complete:
With this page, you've completed the DP Problem-Solving Framework module. You now have a comprehensive methodology for:
This framework transforms DP from a collection of tricks into a principled approach to problem-solving.
Congratulations! You've completed the DP Problem-Solving Framework module. You now possess a systematic methodology for approaching any Dynamic Programming problem—from recognition through implementation to debugging. With practice, this framework becomes second nature, and DP transforms from one of the hardest topics into one of your most powerful tools.