Dynamic Programming

Dynamic Programming (DP) is a powerful problem-solving technique commonly applied to optimization problems (find the minimum cost or maximum value), counting problems (how many ways can something be done?), and existence problems (is a solution even possible?). It works by breaking a complex problem into overlapping subproblems, solving each one exactly once, and storing the results to avoid redundant computation. Think of it as "smart recursion" — remembering what you've already calculated so you never repeat the same work.

What Is Dynamic Programming?#

Dynamic programming solves problems by combining solutions to subproblems. Unlike divide-and-conquer (which splits a problem into independent subproblems that share no work), DP is designed for problems where subproblems overlap — the same calculation appears repeatedly at different points in the recursion. Without memoization or tabulation, a naive recursive solution would recompute the same results exponentially many times.

Rendering diagram...

Real-World Analogies#

  • Climbing Stairs: Instead of counting every possible path from scratch, remember how many ways to reach each step
  • GPS Navigation: Calculate shortest paths to intermediate points, then combine them for the full route
  • Financial Planning: Compute optimal decisions for each year, building on previous years' calculations
  • Game Strategy: Store winning/losing positions so you don't re-analyze the same board state

Key Characteristics#

PropertyDescription
Overlapping SubproblemsThe same subproblem appears multiple times during recursion
Optimal SubstructureAn optimal solution contains optimal solutions to its subproblems
MemoizationTop-down approach: cache results of recursive calls
TabulationBottom-up approach: fill a table iteratively from base cases
Space-Time Trade-offUses extra space (O(n) or O(n²)) to save time (often exponential → polynomial)

The Problem: Overlapping Subproblems#

Consider computing the 5th Fibonacci number using naive recursion. Notice how fib(2) and fib(3) are calculated multiple times:

Rendering diagram...

The red nodes show duplicate calculations. With naive recursion, fib(n) takes O(2ⁿ) time because we recalculate the same values exponentially many times.

Dynamic Programming fixes this by computing each subproblem only once and storing the result.

Two Approaches: Top-Down vs Bottom-Up#

Top-Down (Memoization)#

Start from the main problem and recursively solve subproblems. Store results in a cache (memo) so each subproblem is solved only once.

Rendering diagram...

Bottom-Up (Tabulation)#

Start from base cases and iteratively build up to the main problem. Fill a table from small subproblems to large.

Rendering diagram...

Comparison#

AspectMemoization (Top-Down)Tabulation (Bottom-Up)
DirectionStarts from main problem, goes downStarts from base cases, builds up
ImplementationRecursive with cacheIterative with array/table
Subproblems SolvedOnly needed ones (lazy)All subproblems (eager)
Stack UsageUses call stack (recursion depth)No recursion, constant stack
Ease of WritingOften more intuitiveRequires understanding order
Space OptimizationHarder to optimizeEasier to reduce space

Fibonacci Sequence#

The classic introduction to DP. Let's see how DP transforms exponential time complexity to linear.

Fibonacci Number

Compute the nth Fibonacci number using dynamic programming

Time:O(n)Space:O(n) or O(1)Best:O(1)Worst:O(n)

Climbing Stairs#

Problem: You're climbing a staircase with n steps. Each time you can climb 1 or 2 steps. How many distinct ways can you reach the top?

Rendering diagram...

Key Insight#

To reach step n, you either came from step n-1 (by taking 1 step) or from step n-2 (by taking 2 steps). So the number of distinct ways to reach step n is:

dp[n] = dp[n-1] + dp[n-2]

The base cases are:

  • dp[0] = 1: there is exactly 1 way to be at the starting position — do nothing. This acts as the "seed" that all other counts build on.
  • dp[1] = 1: there is exactly 1 way to reach step 1 — take a single 1-step.

This is exactly the Fibonacci recurrence! Climbing stairs is Fibonacci in disguise.

Climbing Stairs

Count ways to climb n stairs taking 1 or 2 steps at a time

Space-optimized climbing stairs solution

Coin Change#

Problem: Given coin denominations and a target amount, find the minimum number of coins needed to make that amount. You have unlimited coins of each denomination.

Key Insight#

For each amount i, try every coin c. If we can make amount i-c with k coins, then we can make i with k+1 coins:

dp[i] = min(dp[i], dp[i - coin] + 1) for each coin

Coin Change

Find minimum coins needed to make a target amount

Time:O(amount × coins)Space:O(amount)Best:O(amount × coins)Worst:O(amount × coins)

Longest Common Subsequence (LCS)#

Problem: Given two strings, find the length of their longest common subsequence. A subsequence is derived from a string by deleting zero or more characters without changing the order of the remaining characters. A common subsequence of two strings is a subsequence that appears in both. For example, "ACE" is a subsequence of "ABCDE" (delete B and D), so if "ACE" also appears as a subsequence of the second string, it is a common subsequence.

Rendering diagram...

Key Insight#

Compare characters at each position:

  • If s1[i] == s2[j]: extend the LCS by 1 → dp[i][j] = dp[i-1][j-1] + 1
  • If different: take the max of skipping either character → dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Rendering diagram...

Longest Common Subsequence

Find the length of the longest common subsequence of two strings

Time:O(m × n)Space:O(m × n) or O(min(m, n))Best:O(m × n)Worst:O(m × n)

0-1 Knapsack#

Problem: You have a knapsack with capacity W and n items, each with weight and value. Maximize the total value without exceeding capacity. Each item can only be taken once (0 or 1 times).

Rendering diagram...

Key Insight#

For each item i at each capacity w, we have two choices:

  1. Don't take it: Keep the best value achievable using only the previous items → dp[i][w] = dp[i-1][w]
  2. Take it (if it fits): Use the remaining capacity with only the previous items, then add this item's value → dp[i][w] = dp[i-1][w - weight[i]] + value[i]

We take the maximum of both choices. Notice that both cases reference dp[i-1] (the row before item i was considered). This is what enforces the "0 or 1" constraint: we only ever look at prior decisions, so the same item cannot be counted twice.

0-1 Knapsack

Maximize value in knapsack without exceeding capacity

Time:O(n × W)Space:O(n × W) or O(W)Best:O(n × W)Worst:O(n × W)

Common DP Patterns#

Pattern 1: Linear DP (1D)#

Single sequence problems where dp[i] depends on previous elements.

ProblemStateTransition
Fibonaccidp[i] = ith Fibonaccidp[i] = dp[i-1] + dp[i-2]
Climbing Stairsdp[i] = ways to reach step idp[i] = dp[i-1] + dp[i-2]
House Robberdp[i] = max money from first i housesdp[i] = max(dp[i-1], dp[i-2] + nums[i])
Max Subarraydp[i] = max sum ending at idp[i] = max(nums[i], dp[i-1] + nums[i])

Pattern 2: Grid DP (2D)#

Problems on grids where dp[i][j] depends on adjacent cells.

ProblemStateTransition
Unique Pathsdp[i][j] = paths to (i,j)dp[i][j] = dp[i-1][j] + dp[i][j-1]
Min Path Sumdp[i][j] = min cost to (i,j)dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
Edit Distancedp[i][j] = edits for s1[:i] → s2[:j]Match: dp[i-1][j-1], Insert/Delete/Replace

Pattern 3: Two-Sequence DP#

Problems comparing or combining two sequences.

ProblemStateTransition
LCSdp[i][j] = LCS length for s1[:i], s2[:j]Match: dp[i-1][j-1]+1, Else: max(skip either)
Edit Distancedp[i][j] = min editsMatch: dp[i-1][j-1], Else: 1+min(insert,delete,replace)
Interleaving Stringdp[i][j] = can form s3[:i+j]?Match s1[i] or s2[j] to s3[i+j]

Pattern 4: Knapsack / Subset#

Problems involving selecting items with constraints.

VariantDescriptionLoop Direction
0-1 KnapsackEach item used at most onceBackwards (right to left)
Unbounded KnapsackUnlimited copies of each itemForwards (left to right)
Subset SumCan we make target sum?Same as 0-1 Knapsack
Coin ChangeMin coins for amountUnbounded variant

How to Approach DP Problems#

Step 1: Identify if It's a DP Problem#

Look for these clues:

  • Optimization: Find min/max, longest/shortest, count ways
  • Overlapping subproblems: Same calculations needed multiple times
  • Choices: At each step, you make a decision (take/skip, left/right)
  • Sequential decisions: Current choice affects future options

Step 2: Define the State#

Ask: "What information do I need to solve subproblems?"

Problem TypeState Usually Looks Like
Linear sequencedp[i] = answer for first i elements
Two sequencesdp[i][j] = answer for s1[:i] and s2[:j]
Griddp[i][j] = answer at position (i, j)
Knapsackdp[i][w] = answer using first i items with capacity w
Intervaldp[i][j] = answer for range [i, j]

Step 3: Find the Transition#

Ask: "How does the current state relate to previous states?"

Think about:

  • What choices can I make at this step?
  • How do those choices connect to smaller subproblems?
  • What's the recurrence relation?

Step 4: Identify Base Cases#

What are the simplest subproblems with known answers?

  • Empty sequence: dp[0] = 0 or dp[0] = 1
  • Single element: dp[1] = ...
  • First row/column of grid: Often initialized separately

Step 5: Determine Order of Computation#

For bottom-up DP: solve subproblems in an order where dependencies are computed first.

  • Usually left-to-right, top-to-bottom
  • For 0-1 Knapsack with 1D array: right-to-left

Reviewing AI-Generated Code#

Understanding dynamic programming helps you evaluate AI-generated solutions.

What to Verify#

IssueWhat to CheckCommon AI Mistake
Base CasesAll base cases defined and correctMissing or wrong base case leads to wrong results
State DefinitionState captures all needed informationIncomplete state misses subproblem distinctions
Transition LogicRecurrence relation is mathematically correctOff-by-one errors in indices
InitializationDP array initialized to correct valuesUsing 0 when should be infinity or vice versa
Loop OrderEach subproblem is solved after all its dependenciesIterating in the wrong direction — e.g., using a forward loop in 1D knapsack allows the same item to be picked multiple times

Common AI Pitfalls#

Dynamic Programming Bugs

Common mistakes AI makes in DP implementations

Correct coin change with proper initialization

Questions to Ask AI#

When AI generates DP code, verify:

  • "What does dp[i] represent exactly?"
  • "What are the base cases and why?"
  • "Can you prove the recurrence relation is correct?"
  • "Is memoization or tabulation more appropriate here?"

Summary#

ConceptKey Takeaway
Dynamic ProgrammingBreak into subproblems, solve once, store results
Overlapping SubproblemsSame subproblem appears multiple times
Optimal SubstructureOptimal solution contains optimal sub-solutions
Memoization (Top-Down)Recursive + cache, solves only needed subproblems
Tabulation (Bottom-Up)Iterative, fills table from base cases
State DefinitionMost critical step — what info defines a subproblem?
TransitionHow current state relates to previous states

Complexity Improvements with DP#

ProblemBrute ForceWith DP
FibonacciO(2ⁿ)O(n)
Coin ChangeExponentialO(amount × n)
LCSO(2⁽ᵐ⁺ⁿ⁾)O(m × n)
0-1 KnapsackO(2ⁿ)O(n × W)

Dynamic programming transforms exponential-time problems into polynomial-time solutions by remembering what you've already computed. Master the patterns, practice state definitions, and you'll recognize DP opportunities everywhere!