Dynamic Programming

Medium-Hard
Time: O(n) for 1D, O(n*m) for 2D, O(n^2) or O(n^3) for interval DPSpace: O(n) to O(n*m), often optimizable to O(n) or O(1) using rolling array
0/25
problems solved
0%
1.

Introduction to Dynamic Programming

Dynamic Programming (DP) is an optimization technique that solves complex problems by breaking them into overlapping subproblems.

The Core Formula:

DP = Recursion + Memoization
= Avoid solving the same subproblem twice

When to use DP:

  • Problem asks for COUNT of ways (paths, combinations)
  • Problem asks for MIN/MAX (cost, profit, length)
  • You see "take or skip", "include or exclude" decisions
  • Same subproblems are solved multiple times

Two Key Properties:

  1. Optimal Substructure: Optimal solution uses optimal sub-solutions
  2. Overlapping Subproblems: Same subproblem solved multiple times

Example - Fibonacci:

fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2) <- computed here
│ │ └── fib(1)
│ └── fib(2) <- and here again!
└── fib(3) <- and fib(3) computed twice!
├── fib(2)
└── fib(1)
Without memoization: O(2^n)
With memoization: O(n) - each fib(k) computed only once!

The DP Process:

  1. Write recursive solution (brute force)
  2. Add memoization (top-down DP)
  3. Convert to tabulation (bottom-up DP)
  4. Optimize space if possible
2.

The DP Framework - 5 Steps

Use this framework to solve ANY DP problem:

Step 1: Define the State What information do I need to make a decision?

  • Usually: current index, remaining capacity, previous choice
  • State = parameters to your recursive function

Step 2: Define the Recurrence How does current state relate to smaller states?

  • What CHOICES do I have at this state?
  • Express dp[i] in terms of dp[smaller values]

Step 3: Identify Base Cases What are the simplest cases with obvious answers?

  • Empty array, index out of bounds, zero capacity

Step 4: Determine Computation Order Which direction to fill the table?

  • Dependencies must be computed first
  • Usually left->right, bottom->up

Step 5: Extract the Answer Where is the final answer?

  • Usually dp[n], dp[n-1], or dp[0]

Visualization:

Problem: Climbing Stairs (1 or 2 steps)
Step 1 - State: dp[i] = ways to reach step i
Step 2 - Recurrence: dp[i] = dp[i-1] + dp[i-2]
Step 3 - Base: dp[0] = 1, dp[1] = 1
Step 4 - Order: Fill from i=2 to n
Step 5 - Answer: dp[n]
Code
Java
3.

Decision Trees: Visualizing Recursion

Before writing any code, draw the decision tree. This is the most important step in DP!

What is a Decision Tree? A tree where each node represents a STATE, and each branch represents a CHOICE.

Example: Climbing Stairs (n=4)

Question: How many ways to climb 4 stairs (1 or 2 steps at a time)?
climb(4)
/ \
+1 step +2 steps
/ \
climb(3) climb(2)
/ \ / \
climb(2) climb(1) climb(1) climb(0)
/ \ | | |
climb(1) climb(0) ✓ ✓ ✓
| |
✓ ✓
Notice: climb(2) appears TWICE! This is OVERLAPPING SUBPROBLEMS.
Notice: climb(1) appears THREE times!

Example: House Robber [2, 7, 9, 3]

rob(0)
/ \
ROB SKIP
/ \
2+rob(2) rob(1)
/ \ / \
2+ROB 2+SKIP ROB SKIP
/ \ / \
2+9+rob(4) 2+rob(3) 7+rob(3) rob(2)
| | | |
11 ... ... ...
Decision at each house: ROB it or SKIP it
If ROB: add value + jump 2 houses (can't rob adjacent)
If SKIP: move to next house

Example: 0/1 Knapsack - items [(w=1,v=6), (w=2,v=10), (w=3,v=12)], capacity=5

f(0, 5)
/ \
TAKE i=0 SKIP i=0
w=1,v=6
/ \
6+f(1, 4) f(1, 5)
/ \ / \
TAKE SKIP TAKE SKIP
w=2,v=10 w=2,v=10
/ \ / \
6+10+f(2,2) 6+f(2,4) 10+f(2,3) f(2,5)
| | | |
... ... ... ...
State: (item index, remaining capacity)
Choice: TAKE this item or SKIP it

How to Draw a Decision Tree:

  1. Identify the STATE - What changes as we make decisions?
  2. Identify CHOICES - What can we do at each state?
  3. Draw root - Start with the original problem
  4. Branch for each choice - Draw children for each option
  5. Continue until base case - Stop when answer is obvious
  6. Look for repeated subtrees - These are overlapping subproblems!

Key Insight:

Repeated subtrees = Overlapping subproblems = Use DP!
No repeated subtrees = No overlapping = Maybe greedy works

Interactive Decision Tree

DP Decision Tree Visualizer

Watch how recursive calls branch out and see memoization in action

Pure recursion - notice repeated work
Speed:
15
Total Nodes
0
Function Calls
0
Completed
0
Cache Hits
Active
Completed
Cache Hit
Pending
fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(0)
fib(1)
fib(2)
fib(1)
fib(0)
fib(3)
fib(2)
fib(1)
fib(0)
fib(1)
fib(n) = fib(n-1) + fib(n-2)

Without Memoization: Watch how the same subproblems are solved multiple times. This is overlapping subproblems — the key indicator that DP will help!

4.

Deriving Recurrence Relations

The recurrence relation is the heart of any DP solution. Here's how to derive it systematically.

The 4-Question Method:

  1. What decision am I making?
  2. What are my choices?
  3. What happens after each choice?
  4. How do I combine the results?

Example 1: Climbing Stairs

Q1: What decision? -> Which step size to take
Q2: What choices? -> Take 1 step OR take 2 steps
Q3: After each choice? -> Subproblem with remaining stairs
Q4: Combine results? -> ADD (counting ways)
Recurrence:
ways(n) = ways(n-1) + ways(n-2)
└─1 step─┘ └─2 steps─┘

Example 2: House Robber

Q1: What decision? -> Rob this house or skip it
Q2: What choices? -> ROB or SKIP
Q3: After each choice?
- ROB: Get nums[i], skip next, solve for i+2
- SKIP: Get nothing, solve for i+1
Q4: Combine results? -> MAX (maximizing money)
Recurrence:
rob(i) = max(nums[i] + rob(i+2), rob(i+1))
└──ROB this──┘ └─SKIP─┘

Example 3: Coin Change (Minimum coins)

Q1: What decision? -> Which coin to use
Q2: What choices? -> Any coin from the list
Q3: After each choice? -> Subproblem with remaining amount
Q4: Combine results? -> MIN + 1 (minimizing count)
Recurrence:
minCoins(amount) = 1 + min(minCoins(amount - coin)) for each coin
└1 coin used + min for remaining┘

Example 4: Longest Common Subsequence

Q1: What decision? -> Do current chars match or not?
Q2: What choices?
- Match: Take both characters
- No match: Skip from s1 OR skip from s2
Q3: After each choice? -> Smaller subproblem
Q4: Combine results? -> Match: add 1, No match: MAX
Recurrence:
lcs(i, j) =
if s1[i] == s2[j]: 1 + lcs(i-1, j-1) <- chars match!
else: max(lcs(i-1, j), lcs(i, j-1)) <- skip from one string

Common Combination Patterns:

| Problem Type | Combine With |
|---------------------------|--------------|
| Count ways | ADD (+) |
| Minimize cost/count | MIN |
| Maximize value/length | MAX |
| Check possibility | OR (||) |
| Check all conditions | AND (&&) |

Writing the Recurrence - Template:

f(state) = combine(
result_of_choice_1,
result_of_choice_2,
...
)
where:
result_of_choice_k = value_from_choice + f(new_state_after_choice)

Interactive Recurrence Builder

Recurrence Relation Builder

Learn to derive recurrence relations step-by-step

Climbing Stairs

You can climb 1 or 2 steps. How many ways to reach step n?

?The 4-Question Method

1What decision am I making?
2What are my choices?
3What happens after each choice?
4How do I combine results?
Combination Patterns:
+Count ways
max()Maximize value
min()Minimize cost
||Check possibility
5.

Complete DP Transformation: Recursion -> O(1) Space

Let's transform a problem through ALL FOUR stages using House Robber as our example.

Problem: Given array [2, 7, 9, 3, 1], find max money without robbing adjacent houses.

Answer: 12 (rob houses with values 2, 9, 1)


STAGE 1: PURE RECURSION (Brute Force)

Thought process:
- At each house, I decide: ROB or SKIP
- If ROB: take money + solve for 2 houses later (can't rob adjacent)
- If SKIP: solve for next house
- Return the maximum
Time: O(2^n) - each house branches into 2 choices
Space: O(n) - recursion stack

Decision Tree for [2, 7, 9, 3, 1]:

rob(0)
/ \
ROB[0] SKIP[0]
+2 |
| |
rob(2) rob(1)
/ \ / \
ROB[2] SKIP ROB[1] SKIP
+9 | +7 |
| | | |
rob(4) rob(3) rob(3) rob(2) <- OVERLAPPING!
+1 ... ... ...
See rob(2) and rob(3) computed multiple times!

STAGE 2: MEMOIZATION (Top-Down DP)

Key insight: Same subproblems solved repeatedly!
Solution: Cache results in a memo array
Before computing rob(i):
1. Check if already in memo -> return cached
2. Otherwise compute, store in memo, return
Time: O(n) - each state computed only once
Space: O(n) - memo array + recursion stack

Execution trace with memo:

rob(0): not in memo
-> rob(2): not in memo
-> rob(4): not in memo, compute = 1, memo[4] = 1
-> rob(3): not in memo, compute = 3, memo[3] = 3
-> rob(2) = max(9+1, 3) = 10, memo[2] = 10
-> rob(1): not in memo
-> rob(3): IN MEMO! return 3 <- CACHE HIT!
-> rob(2): IN MEMO! return 10 <- CACHE HIT!
-> rob(1) = max(7+3, 10) = 10, memo[1] = 10
-> rob(0) = max(2+10, 10) = 12, memo[0] = 12
Answer: 12

STAGE 3: TABULATION (Bottom-Up DP)

Key insight: Instead of recursing, build solution from base cases UP
Process:
1. Define dp[i] = max money from house i to end
2. Start from END (base cases)
3. Work backwards to START
Time: O(n)
Space: O(n) - just the dp array (no recursion stack!)

Filling the DP table:

nums = [2, 7, 9, 3, 1]
Start from right:
dp[4] = 1 (only house 4)
dp[3] = max(3+0, 1) = 3 (rob 3 OR skip to 4)
dp[2] = max(9+1, 3) = 10 (rob 2 OR skip to 3)
dp[1] = max(7+3, 10) = 10 (rob 1 OR skip to 2)
dp[0] = max(2+10, 10) = 12 (rob 0 OR skip to 1)
dp = [12, 10, 10, 3, 1]
Answer!

STAGE 4: SPACE OPTIMIZATION

Key insight: dp[i] only needs dp[i+1] and dp[i+2]
We don't need the entire array!
Use just 2 variables:
- next1 = dp[i+1] (skip to next house)
- next2 = dp[i+2] (rob current, skip adjacent)
Time: O(n)
Space: O(1) <- OPTIMAL!

Trace with 2 variables:

nums = [2, 7, 9, 3, 1]
Start: next1 = 0, next2 = 0
i=4: curr = max(1+0, 0) = 1
next2 = 0, next1 = 1
i=3: curr = max(3+0, 1) = 3
next2 = 1, next1 = 3
i=2: curr = max(9+1, 3) = 10
next2 = 3, next1 = 10
i=1: curr = max(7+3, 10) = 10
next2 = 10, next1 = 10
i=0: curr = max(2+10, 10) = 12 <- ANSWER!
next2 = 10, next1 = 12
Final answer: 12

Summary: The 4 Stages

┌─────────────────┬────────────┬────────────┐
│ Stage │ Time │ Space │
├─────────────────┼────────────┼────────────┤
│ 1. Recursion │ O(2^n) │ O(n) │
│ 2. Memoization │ O(n) │ O(n) │
│ 3. Tabulation │ O(n) │ O(n) │
│ 4. Optimized │ O(n) │ O(1) │
└─────────────────┴────────────┴────────────┘
Progression:
Brute Force -> Cache Results -> Iterate -> Minimize Variables
Code
Java

Interactive Transformation

DP Transformation: Recursion → O(1) Space

House Robber: [2, 7, 9, 3, 1] → Maximum: $12

Time
O(2ⁿ)
Space
O(n)
Progress
0 / 14
Speed:

Call Stack

Stack empty

Computed Results

Pure Recursion: Brute force - every call branches into more calls

Side-by-Side Race

DP Approaches: Side-by-Side Race

Computing fib(6) = 8 — Watch all 4 approaches compete!

Speed:

Pure Recursion

O(2ⁿ)
Function Calls0
Call Stack:
⚠️ Exponentially slow! Many duplicate calls...

Memoization

O(n)
Calls0
Cache: (hits: 0)
?
?
?
?
?
?
?
✓ Cache prevents duplicate work!

Tabulation

O(n)
Index0/6
DP Array:
[0]-
[1]-
[2]-
[3]-
[4]-
[5]-
[6]-
✓ No recursion overhead, just iteration

Space Optimized

O(1) space
Index0/6
Just 2 variables:
prev2
0
prev1
1
✓ Minimal memory, maximum efficiency!
6.

Memoization vs Tabulation

Two ways to implement DP:

Memoization (Top-Down)

  • Start from the main problem, recurse down
  • Cache results as you go
  • More intuitive - matches recursive thinking
  • Only computes needed subproblems

Tabulation (Bottom-Up)

  • Start from base cases, build up
  • Fill a table iteratively
  • Usually faster (no recursion overhead)
  • Easier to optimize space

Comparison:

Memoization Tabulation
(Top-Down) (Bottom-Up)
Direction: Large -> Small Small -> Large
Structure: Recursive + Cache Iterative + Table
Computes: Only needed states All states
Stack: Uses call stack No recursion
Space Opt: Harder Easier

When to use which:

  • Start with memoization (easier to think)
  • Convert to tabulation for performance
  • Use tabulation when you need space optimization

Conversion Process:

Memoization: Tabulation:
f(n) { for i = base to n:
if cached: return cache[n] dp[i] = recurrence
result = recurrence return dp[n]
cache[n] = result
return result
}
Code
Java
7.

1D DP - Linear Problems

Problems where state depends on previous elements in a sequence.

Common Patterns:

  1. House Robber - Take or skip decision
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
"Max of: skip this house OR rob this + best from 2 houses ago"
  1. Coin Change - Minimize count
dp[amount] = min(dp[amount - coin] + 1) for each coin
"Min coins = 1 + min coins for remaining amount"
  1. Word Break - Can we segment?
dp[i] = true if dp[j] && s[j:i] is a word
"Position i reachable if some j reachable AND j to i is a word"

Visualization - House Robber:

nums = [2, 7, 9, 3, 1]
i=0: dp[0] = 2 (rob first house)
i=1: dp[1] = max(7, 2) = 7 (rob second or first)
i=2: dp[2] = max(7, 2+9) = 11 (skip or rob)
i=3: dp[3] = max(11, 7+3) = 11
i=4: dp[4] = max(11, 11+1) = 12
Answer: 12 (rob houses 0, 2, 4)
Code
Java
8.

2D DP - Grid & Two Sequences

Problems with two dimensions: grids, two strings, or two arrays.

Pattern 1: Grid Paths

dp[i][j] = ways/cost to reach cell (i, j)
dp[i][j] = dp[i-1][j] + dp[i][j-1] (for unique paths)

Pattern 2: Two Strings (LCS, Edit Distance)

dp[i][j] = answer for first i chars of s1 and first j chars of s2

Visualization - Unique Paths:

3x3 grid:
0 1 2
--------
0| 1 1 1
1| 1 2 3
2| 1 3 6
dp[2][2] = 6 ways to reach bottom-right

Visualization - LCS (Longest Common Subsequence):

s1 = "ABCD", s2 = "AEBD"
"" 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 3
LCS = "ABD", length = 3
Recurrence:
if s1[i] == s2[j]: dp[i][j] = dp[i-1][j-1] + 1
else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Code
Java

Interactive 2D Table

2D DP Table Visualizer

Watch the table fill cell by cell

Find longest subsequence present in both strings

s1 = "ABCD"s2 = "AEBD"
Speed:
Step 0/25
""
A
E
B
D
""
A
B
C
D
9.

0/1 Knapsack Pattern

Classic DP pattern: Given items with weights/values, maximize value within capacity.

The Pattern:

  • For each item: TAKE it or SKIP it
  • State: (current item index, remaining capacity)
  • Decision: Include this item or not?

Recurrence:

dp[i][w] = max value using first i items with capacity w
if weight[i] > w:
dp[i][w] = dp[i-1][w] // Can't take, skip
else:
dp[i][w] = max(
dp[i-1][w], // Skip item
dp[i-1][w-weight[i]] + value[i] // Take item
)

Visualization:

items: [(weight=1, value=6), (w=2, v=10), (w=3, v=12)]
capacity: 5
w=0 w=1 w=2 w=3 w=4 w=5
i=0 0 6 6 6 6 6
i=1 0 6 10 16 16 16
i=2 0 6 10 16 18 22
Max value = 22 (take items 1 and 2)

Related Problems:

  • Partition Equal Subset Sum (can we split into equal halves?)
  • Target Sum (count ways to reach target)
  • Coin Change (unbounded - can reuse items)
Code
Java

Interactive Knapsack

0/1 Knapsack Visualizer

Capacity: 7kg — Select items to maximize value

Available Items

Phone
⚖️ 1kg💰 $1
Laptop
⚖️ 3kg💰 $4
Camera
⚖️ 4kg💰 $5
Tablet
⚖️ 2kg💰 $3

Your Knapsack

Items will appear here...
Weight Used0/7kg
💰 $0
Speed:
Item\Cap
0kg
1kg
2kg
3kg
4kg
5kg
6kg
7kg
0
0
0
0
0
0
0
0
Phone
Laptop
Camera
Tablet
Progress: 0/32 cells
10.

Longest Increasing Subsequence (LIS)

Find the longest subsequence where elements are in increasing order.

Problem: [10, 9, 2, 5, 3, 7, 101, 18] -> LIS = [2, 3, 7, 101] or [2, 3, 7, 18], length = 4

O(n²) DP Solution:

dp[i] = length of LIS ending at index i
For each i, check all j < i:
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)

Visualization:

nums: [10, 9, 2, 5, 3, 7, 101, 18]
dp: [1, 1, 1, 2, 2, 3, 4, 4]
i=3 (5): check j=0,1,2
j=2: nums[2]=2 < 5, dp[3] = max(1, dp[2]+1) = 2
i=5 (7): check j=0..4
j=3: nums[3]=5 < 7, dp[5] = max(1, dp[3]+1) = 3
j=4: nums[4]=3 < 7, dp[5] = max(3, dp[4]+1) = 3

O(n log n) Optimization: Maintain a sorted array of smallest tail elements. Use binary search to find position.

Process [10, 9, 2, 5, 3, 7, 101, 18]:
tails = [10] -> [9] -> [2] -> [2,5] -> [2,3] -> [2,3,7] -> [2,3,7,101] -> [2,3,7,18]
Length = 4
Code
Java
11.

DP on Strings

Common patterns for string DP problems.

Pattern 1: Single String

  • Palindrome problems
  • dp[i][j] = property of substring s[i..j]

Pattern 2: Two Strings

  • LCS, Edit Distance, Interleaving
  • dp[i][j] = property using s1[0..i] and s2[0..j]

Longest Palindromic Substring:

dp[i][j] = true if s[i..j] is palindrome
Base: dp[i][i] = true (single char)
dp[i][i+1] = (s[i] == s[i+1])
Recurrence:
dp[i][j] = (s[i] == s[j]) AND dp[i+1][j-1]

Visualization:

s = "babad"
j=0 1 2 3 4
i=0 T F T F F b a b a d
i=1 T F T F
i=2 T F F
i=3 T F
i=4 T
Longest palindrome: "bab" or "aba" (length 3)
Code
Java
12.

Common DP Patterns Summary

Quick reference for identifying DP problem types:

By Problem Type:

PatternClue WordsRecurrence
Fibonacci-like"stairs", "ways"dp[i] = dp[i-1] + dp[i-2]
Take/Skip"rob", "schedule"dp[i] = max(dp[i-1], dp[i-2] + val)
Knapsack"capacity", "weight"dp[i][w] = max(skip, take)
LCS"common", "subsequence"match: +1, else: max
Edit Distance"transform", "operations"min(replace, insert, delete)
LIS"increasing", "longest"dp[i] = max(dp[j]+1) for j<i
Palindrome"palindrome"dp[i][j] = ends match && inner is palindrome
Grid"paths", "minimum cost"dp[i][j] = f(dp[i-1][j], dp[i][j-1])

Space Optimization Rules:

1D depends on i-1, i-2 -> Use 2 variables
2D depends on i-1 row -> Use 1D array
2D depends on i-1, j-1 -> Use 1D + temp variable

Common Mistakes:

  • Wrong base case initialization
  • Wrong iteration direction (must compute dependencies first)
  • Off-by-one errors in indices
  • Forgetting to handle edge cases (empty input)

DP vs Greedy:

Greedy: Make locally optimal choice, never reconsider
DP: Consider all choices, pick best using subproblem results
Use Greedy when: local optimal = global optimal
Use DP when: need to try multiple paths to find optimal

Test Your Knowledge

Take a quick 15question quiz to test what you've learned.