π§ββοΈ Dynamic Programming: The Magic of Remembering
Once upon a time, there was a wizard who was terrible at math. Every time someone asked him βWhatβs 3 + 5?β, he would count on his fingers. Then theyβd ask βWhatβs 3 + 5 again?β and heβd count ALL OVER. Silly wizard!
One day, a clever student said: βWhy not write down the answer the first time? Then just READ it!β
The wizardβs eyes lit up. This simple trickβremembering answers instead of recalculating themβis the heart of Dynamic Programming.
π― What is Dynamic Programming?
Dynamic Programming (DP) is like having a super memory.
Instead of solving the same puzzle over and over, you:
- Solve it once
- Write down the answer
- Look it up next time
Thatβs it! Youβre trading a bit of paper (memory) for a LOT of time savings.
Real Life Example π
Imagine you run a pizza shop. A customer asks:
βHow much for 2 slices?β
You calculate: $3 per slice Γ 2 = $6
Another customer asks the SAME thing. Do you calculate again? No! You just remember: β2 slices = $6β
βββββββββββββββββββββββββββββββ
β WITHOUT DP (Silly Wizard) β
β Calculate β Forget β β
β Calculate β Forget β β
β Calculate... (forever!) β
βββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββ
β WITH DP (Smart Student) β
β Calculate β Remember β β
β Look up β Done! π β
βββββββββββββββββββββββββββββββ
π When Does DP Work?
DP only works when your problem has two magical properties:
1. Overlapping Subproblems π
The same small puzzle appears MANY times.
Example: Fibonacci Numbers
fib(5) needs fib(4) and fib(3)
fib(4) needs fib(3) and fib(2)
fib(3) needs fib(2) and fib(1)
Notice fib(3) appears twice? Thatβs overlap!
2. Optimal Substructure ποΈ
The best answer to the BIG puzzle = combining best answers to SMALL puzzles.
Example: Shortest Path The shortest path from A to C through B = (Shortest AβB) + (Shortest BβC)
π οΈ Two Ways to Build DP Solutions
Think of building a staircase. You can start from the bottom or the top!
Approach 1: Top-Down (Memoization) β¬οΈ
βAsk, then remember.β
You start with the BIG question and break it down. When you find an answer, you SAVE it.
# Memory box to store answers
memo = {}
def fib(n):
# Already solved? Just return it!
if n in memo:
return memo[n]
# Base cases (we know these)
if n <= 1:
return n
# Solve and SAVE
memo[n] = fib(n-1) + fib(n-2)
return memo[n]
How it feels:
"What's fib(5)?"
β "Hmm, I need fib(4) and fib(3)..."
β "What's fib(4)? I need fib(3) and fib(2)..."
β (keeps asking smaller questions)
β "Oh! fib(1) = 1, fib(0) = 0. Done!"
β (now I remember fib(3)!)
β "fib(5) = 5. Saved!"
Approach 2: Bottom-Up (Tabulation) β¬οΈ
βBuild from ground up.β
You start with the SMALLEST answers and build up to the big one.
def fib(n):
if n <= 1:
return n
# Build a table
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
# Fill from small to big
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
How it feels:
"Let me build up step by step..."
dp[0] = 0 β
dp[1] = 1 β
dp[2] = 0 + 1 = 1 β
dp[3] = 1 + 1 = 2 β
dp[4] = 1 + 2 = 3 β
dp[5] = 2 + 3 = 5 β
"Answer: 5!"
Which One to Pick? π€
| Top-Down | Bottom-Up |
|---|---|
| More natural to think | Slightly faster |
| Uses recursion | Uses loops |
| Only solves whatβs needed | Solves everything |
| Good for exploring | Good for final code |
π¦ State: The Heart of Every DP Problem
State = What information do I need to describe my current situation?
Think of it like saving a video game. What do you need to save?
- Your position
- Your health
- Your inventory
- The level youβre on
In DP, state is the information you store to describe a subproblem.
Example: Climbing Stairs πͺ
You can climb 1 or 2 steps at a time. How many ways to reach step N?
State: dp[i] = number of ways to reach step i
def climb_stairs(n):
dp = [0] * (n + 1)
dp[0] = 1 # 1 way to stay at ground
dp[1] = 1 # 1 way to reach step 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
Example: Knapsack Problem π
You have items with weights and values. Whatβs the maximum value you can carry?
State: dp[i][w] = max value using first i items with capacity w
Two pieces of information needed:
- Which items weβve considered
- How much weight capacity remains
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(capacity+1):
# Don't take item i
dp[i][w] = dp[i-1][w]
# Take item if it fits
if weights[i-1] <= w:
take = values[i-1] + dp[i-1][w-weights[i-1]]
dp[i][w] = max(dp[i][w], take)
return dp[n][capacity]
Choosing Good States π―
Ask yourself:
- What changes between subproblems?
- What do I need to know to solve the current problem?
- Whatβs the MINIMUM information required?
Bad state: Too much info (wasteful) Good state: Just enough to make decisions
π State Transition: The Magic Formula
Transition = How do I get from one state to another?
This is the RECIPE that connects small answers to big answers.
Pattern: Current = f(Previous States)
dp[current] = some_function(dp[smaller_problems])
Fibonacci Transition
dp[i] = dp[i-1] + dp[i-2]
Current = sum of two previous
Climbing Stairs Transition
dp[i] = dp[i-1] + dp[i-2]
To reach step i: come from step i-1 OR step i-2
Knapsack Transition
dp[i][w] = max(
dp[i-1][w], # Don't take item
dp[i-1][w-wt] + val # Take item
)
Choose the better option: skip or take
Writing Transitions: The 3-Step Dance π
Step 1: Identify your choices at each step
- Can I take this item or not?
- Can I go left or right?
- Can I use this coin or not?
Step 2: Write the formula for each choice
- If I take it: value + previous state
- If I skip it: just previous state
Step 3: Combine choices (usually max or min or sum)
# General pattern
dp[current] = combine(
choice_1_result,
choice_2_result,
...
)
π Space Optimization: Travel Light!
Hereβs a secret: Sometimes you donβt need to remember EVERYTHING.
Observation: Many DP solutions only look at the previous 1 or 2 states.
Fibonacci: From O(n) to O(1) Space
Before (stores everything):
dp = [0] * (n + 1)
dp[0], dp[1] = 0, 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
After (only keeps what we need):
if n <= 1:
return n
prev2, prev1 = 0, 1
for i in range(2, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
We only need two variables!
Knapsack: From O(nΓW) to O(W) Space
Key insight: Each row only needs the previous row.
Before (2D table):
dp = [[0]*(W+1) for _ in range(n+1)]
After (1D table, traverse backwards):
dp = [0] * (W + 1)
for i in range(n):
# BACKWARDS to avoid using updated values
for w in range(W, weights[i]-1, -1):
dp[w] = max(dp[w], dp[w-weights[i]] + values[i])
Why Backwards? π
When we go left-to-right, we overwrite values we still need!
Forward (WRONG):
dp[3] = dp[3-2] + val # Uses NEW dp[1]
Backward (CORRECT):
dp[5] = dp[5-2] + val # Uses OLD dp[3]
dp[3] = dp[3-2] + val # Uses OLD dp[1]
πΊοΈ The DP Roadmap
graph TD A["π― Identify Problem"] --> B{Has Overlapping<br>Subproblems?} B -->|Yes| C{Has Optimal<br>Substructure?} B -->|No| X["β Not DP"] C -->|Yes| D["β Use DP!"] C -->|No| X D --> E["Define State"] E --> F["Write Transition"] F --> G["Set Base Cases"] G --> H["Choose Approach"] H --> I["Top-Down"] H --> J["Bottom-Up"] I --> K["Optimize Space?"] J --> K K --> L["π Solution!"]
π§ͺ Classic Example: Longest Common Subsequence
Find the longest sequence that appears in both strings.
Strings: βABCDEβ and βACEβ Answer: βACEβ (length 3)
State Definition
dp[i][j] = LCS length of first i chars of string1 and first j chars of string2
Transition
if s1[i-1] == s2[j-1]:
# Characters match! Add 1 to previous
dp[i][j] = dp[i-1][j-1] + 1
else:
# No match. Take best of skipping either char
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Full Code
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] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
Space-Optimized Version
def lcs_optimized(s1, s2):
m, n = len(s1), len(s2)
prev = [0] * (n + 1)
curr = [0] * (n + 1)
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = max(prev[j], curr[j-1])
prev, curr = curr, [0]*(n+1)
return prev[n]
π Summary: Your DP Toolkit
| Concept | What It Means | Example |
|---|---|---|
| DP | Remember answers | Memo dictionary |
| Top-Down | Start big, go small | Recursion + memo |
| Bottom-Up | Start small, build up | Loops + table |
| State | Info to describe problem | dp[i], dp[i][j] |
| Transition | How states connect | dp[i] = dp[i-1] + dp[i-2] |
| Space Opt | Keep only whatβs needed | Two variables, 1D array |
π Youβre Ready!
Remember the wizard from our story? After learning to write down answers, he became the fastest mathematician in the kingdom!
DP is not magicβitβs being smart about memory.
When you see a problem that:
- Has the same subproblems appearing again and again
- Can be solved by combining smaller solutions
β¦you now know what to do. Define your state, write your transition, and let the computer do the remembering!
Happy Coding! π
