Chapter 6.1: Introduction to Dynamic Programming
๐ Before You Continue: Make sure you understand recursion (Chapter 2.3), arrays/vectors (Chapters 2.3โ3.1), and basic loop patterns (Chapter 2.2). DP builds directly on recursion concepts.
Dynamic programming (DP) is often described as "clever recursion with memory." Let's build up this intuition from scratch, starting with the simplest possible example: Fibonacci numbers.
๐ก Key Insight: DP solves problems with two properties:
- Overlapping subproblems โ the same sub-computation appears many times
- Optimal substructure โ the optimal solution to a big problem can be built from optimal solutions to smaller problems
When both are true, DP transforms exponential time into polynomial time.
6.1.1 The Problem with Naive Recursion
The Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
Definition: F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2) for n โฅ 2.
Visual: Fibonacci Recursion Tree and Memoization
The recursion tree for fib(5) exposes the problem: fib(3) is computed twice (red nodes). Memoization caches each result the first time it's computed, reducing 2^N calls to just N unique calls โ the fundamental insight behind dynamic programming.
The static diagram above shows how memoization eliminates redundant computations: each unique subproblem is solved only once and its result is cached for future lookups.
The naรฏve recursive implementation:
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n-1) + fib(n-2); // recursive
}
This is correct, but devastatingly slow. Let's see why:
fib(5)
โโโ fib(4)
โ โโโ fib(3)
โ โ โโโ fib(2)
โ โ โ โโโ fib(1) = 1
โ โ โ โโโ fib(0) = 0
โ โ โโโ fib(1) = 1
โ โโโ fib(2) โ COMPUTED AGAIN!
โ โโโ fib(1) = 1
โ โโโ fib(0) = 0
โโโ fib(3) โ COMPUTED AGAIN!
โโโ fib(2) โ COMPUTED AGAIN!
โ โโโ fib(1) = 1
โ โโโ fib(0) = 0
โโโ fib(1) = 1
fib(3) is computed twice. fib(2) three times. For fib(50), the number of calls exceeds 10^10. This is exponential time: O(2^n).
The core insight: we're recomputing the same subproblems over and over. DP fixes this.
6.1.2 Memoization (Top-Down DP)
Memoization = recursion + cache. Before computing, check if we've already computed this value. If yes, return the cached result. If no, compute it, cache it, return it.
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100;
long long memo[MAXN]; // memo[n] = F(n), or -1 if not yet computed
bool computed[MAXN]; // track which values are computed
long long fib_memo(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
if (computed[n]) return memo[n]; // already computed? return cached value
memo[n] = fib_memo(n-1) + fib_memo(n-2); // compute and cache
computed[n] = true;
return memo[n];
}
int main() {
memset(computed, false, sizeof(computed)); // initialize cache as empty
for (int i = 0; i <= 20; i++) {
cout << "F(" << i << ") = " << fib_memo(i) << "\n";
}
return 0;
}
Or using -1 as the sentinel:
๐ ่ฏดๆ๏ผ ไปฅไธๆฏไธไธๆ
fib_memo็ญไปท็ๅฆไธ็งๅๆณใๅบๅซๅจไบ๏ผโ ็จ-1ไฝไธบ"ๆช่ฎก็ฎ"็ๅจๅ ตๅผ๏ผ็ๅปๅ็ฌ็computed[]ๆฐ็ป๏ผโก ๅฝๆฐๅๆนไธบfib๏ผๅๆณๆด็ฎๆดใไธค็งๅๆณๅจๅ่ฝไธๅฎๅ จ็ธๅ๏ผ่ฏทๅฟๅฐไธค็งๅๆณ็ไปฃ็ ็ๆฎตๆทท็จ๏ผๅฎไปฌๅ่ชๆฅๆ็ฌ็ซ็ๅ จๅฑmemoๆฐ็ป๏ผใ
// ๅๆณไบ๏ผ-1 ๅจๅ
ตๅผ๏ผ็ญไปทไบไธๆ fib_memo๏ผๆด็ฎๆด๏ผ
const int MAXN = 100;
long long memo[MAXN];
long long fib(int n) {
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
return memo[n] = fib(n-1) + fib(n-2);
}
int main() {
fill(memo, memo + MAXN, -1LL); // ๅฐๆๆๅผๅๅงๅไธบ -1๏ผ"ๆช่ฎก็ฎ"ๆ ่ฎฐ๏ผ
cout << fib(50) << "\n"; // 12586269025
return 0;
}
Now each value is computed exactly once. Time complexity: O(N). ๐
6.1.3 Tabulation (Bottom-Up DP)
Tabulation builds the answer from the ground up โ compute small subproblems first, use them to compute larger ones.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n = 50;
vector<long long> dp(n + 1);
// Base cases
dp[0] = 0;
dp[1] = 1;
// Fill the table bottom-up
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2]; // use already-computed values
}
cout << dp[n] << "\n"; // 12586269025
return 0;
}
We can even optimize space: since each Fibonacci number only depends on the previous two, we only need O(1) space:
long long a = 0, b = 1;
for (int i = 2; i <= n; i++) {
long long c = a + b;
a = b;
b = c;
}
cout << b << "\n";
Memoization vs. Tabulation
Two approaches compared for fib(4):
๐ก ๆ ธๅฟๅบๅซ๏ผ Top-Down ๆ้่ฎก็ฎ๏ผๅช็ฎ็จๅฐ็ๅญ้ฎ้ข๏ผ๏ผBottom-Up ๅ จ้ๅกซ่กจ๏ผๆ้กบๅบ็ฎๆๆๅญ้ฎ้ข๏ผใไธค่ ๆถ้ดๅคๆๅบฆ็ธๅ๏ผไฝ Bottom-Up ๆ ้ๅฝๆ ๅผ้ใ
| Aspect | Memoization (Top-Down) | Tabulation (Bottom-Up) |
|---|---|---|
| Approach | Recursive with caching | Iterative table filling |
| Memory usage | Only computed states | All states (even unused) |
| Implementation | Often more intuitive | May need to figure out fill order |
| Stack overflow risk | Yes (deep recursion) | No |
| Speed | Slightly slower (function call overhead) | Slightly faster |
| Subproblems computed | Only reachable ones | All (even unreachable) |
| Debugging | Easier (follow recursion) | Harder (need correct fill order) |
| USACO preference | Great for understanding | Great for final solutions |
๐ USACO Tip: In competition, bottom-up tabulation is slightly preferred because it avoids potential stack overflow (critical on problems with N = 10^5) and is often faster. But start with top-down if you're having trouble seeing the recurrence โ it's a great way to think through the problem.
In competitive programming, both are valid. Practice both until you can convert easily between them.
6.1.4 The DP Recipe
Every DP problem follows the same recipe:
The 4-step DP recipe โ from state definition to space optimization:
- Define the state: What information uniquely describes a subproblem?
- Define the recurrence: How does
dp[state]depend on smaller states? - Identify base cases: What are the simplest subproblems with known answers?
- Determine order: In what order should we fill the table?
Let's apply this to Fibonacci:
- State:
dp[i]= the i-th Fibonacci number - Recurrence:
dp[i] = dp[i-1] + dp[i-2] - Base cases:
dp[0] = 0,dp[1] = 1 - Order: i from 2 to n (each depends on smaller i)
6.1.5 Coin Change โ Classic DP
Problem: You have coins of denominations coins[]. What is the minimum number of coins needed to make amount W? You can use each coin type unlimited times.
Example: coins = [1, 5, 6, 9], W = 11
Let's first try the greedy approach (always pick the largest coin โค remaining):
- Greedy: 9 + 1 + 1 = 3 coins โ not optimal!
- Optimal: 5 + 6 = 2 coins โ DP finds this
This is why greedy fails here and we need DP.
Visual: Coin Change DP Table
The DP table shows how dp[i] (minimum coins to make amount i) is filled left to right. For coins {1,3,4}, notice that dp[3]=1 (just use coin 3) and dp[6]=2 (use two 3s). Each cell builds on previous cells using the recurrence.
This static reference shows the complete coin change DP table, with arrows indicating how each cell's value depends on previous cells via the recurrence dp[w] = 1 + min(dp[w-c]).
DP Definition
Coin Change state transitions for coins = {1, 5, 6}, W = 7:
๐ก Transition direction: Each
dp[w]transitions fromdp[w-c](the remaining amount after using coin c). The arrows show dependency: dp[w] depends on cells to its left.
- State:
dp[w]= minimum coins to make exactly amountw - Recurrence:
dp[w] = 1 + min over all coins c where c โค w: dp[w - c](use coin c, then solve the remaining w-c optimally) - Base case:
dp[0] = 0(zero coins to make amount 0) - Answer:
dp[W] - Order: fill w from 1 to W
Complete Walkthrough: coins = [1, 5, 6, 9], W = 11
dp[0] = 0 (base case)
dp[1]: try coin 1: dp[0]+1=1 โ dp[1] = 1
dp[2]: try coin 1: dp[1]+1=2 โ dp[2] = 2
dp[3]: try coin 1: dp[2]+1=3 โ dp[3] = 3
dp[4]: try coin 1: dp[3]+1=4 โ dp[4] = 4
dp[5]: try coin 1: dp[4]+1=5
try coin 5: dp[0]+1=1 โ dp[5] = 1 โ use the 5-coin!
dp[6]: try coin 1: dp[5]+1=2
try coin 5: dp[1]+1=2
try coin 6: dp[0]+1=1 โ dp[6] = 1 โ use the 6-coin!
dp[7]: try coin 1: dp[6]+1=2
try coin 5: dp[2]+1=3
try coin 6: dp[1]+1=2 โ dp[7] = 2 โ 1+6 or 6+1
dp[8]: try coin 1: dp[7]+1=3
try coin 5: dp[3]+1=4
try coin 6: dp[2]+1=3 โ dp[8] = 3
dp[9]: try coin 1: dp[8]+1=4
try coin 5: dp[4]+1=5
try coin 6: dp[3]+1=4
try coin 9: dp[0]+1=1 โ dp[9] = 1 โ use the 9-coin!
dp[10]: try coin 1: dp[9]+1=2
try coin 5: dp[5]+1=2
try coin 6: dp[4]+1=5
try coin 9: dp[1]+1=2 โ dp[10] = 2 โ 1+9, 5+5, or 9+1
dp[11]: try coin 1: dp[10]+1=3
try coin 5: dp[6]+1=2
try coin 6: dp[5]+1=2
try coin 9: dp[2]+1=3 โ dp[11] = 2 โ 5+6 or 6+5!
dp table: [0, 1, 2, 3, 4, 1, 1, 2, 3, 1, 2, 2]
Answer: dp[11] = 2 (coins 5 and 6) โ
// Solution: Minimum Coin Change โ O(N ร W)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, W;
cin >> n >> W;
vector<int> coins(n);
for (int &c : coins) cin >> c;
const int INF = 1e9;
vector<int> dp(W + 1, INF); // dp[w] = min coins to make w
dp[0] = 0; // base case
// Step 1: Fill dp table bottom-up
for (int w = 1; w <= W; w++) {
for (int c : coins) {
if (c <= w && dp[w - c] != INF) {
dp[w] = min(dp[w], dp[w - c] + 1); // โ KEY LINE
}
}
}
// Step 2: Output result
if (dp[W] == INF) {
cout << "Impossible\n";
} else {
cout << dp[W] << "\n";
}
return 0;
}
Sample Input:
4 11
1 5 6 9
Sample Output:
2
Complexity Analysis:
- Time:
O(N ร W)โ for each amount w (1..W), try all N coins - Space:
O(W)โ just the dp array
Reconstructing the Solution
How do we print which coins were used? Track parent[w] = which coin was used last:
vector<int> dp(W + 1, INF);
vector<int> lastCoin(W + 1, -1); // which coin gave optimal solution for w
dp[0] = 0;
for (int w = 1; w <= W; w++) {
for (int c : coins) {
if (c <= w && dp[w-c] + 1 < dp[w]) {
dp[w] = dp[w-c] + 1;
lastCoin[w] = c; // record that coin c was used
}
}
}
// Trace back the solution
vector<int> solution;
int w = W;
while (w > 0) {
solution.push_back(lastCoin[w]);
w -= lastCoin[w];
}
for (int c : solution) cout << c << " ";
cout << "\n";
6.1.6 Number of Ways โ Coin Change Variant
Problem: How many different ways can you make amount W using the given coins? (Order matters: [1,5] and [5,1] are different.)
// Ordered ways (permutations โ order matters)
vector<long long> ways(W + 1, 0);
ways[0] = 1; // one way to make 0: use no coins
for (int w = 1; w <= W; w++) {
for (int c : coins) {
if (c <= w) {
ways[w] += ways[w - c]; // โ KEY LINE
}
}
}
If order doesn't matter (combinations โ [1,5] same as [5,1]):
// Unordered ways (combinations โ order doesn't matter)
vector<long long> ways(W + 1, 0);
ways[0] = 1;
for (int c : coins) { // outer loop: coins (each coin is considered once)
for (int w = c; w <= W; w++) { // inner loop: amounts
ways[w] += ways[w - c];
}
}
๐ก Key Insight: The order of loops matters for counting combinations vs. permutations! When coins are in the outer loop, each coin is "introduced" once and order is ignored. When amounts are in the outer loop, each amount is formed fresh each time, allowing all orderings.
โ ๏ธ Common Mistakes in Chapter 6.1
- Initializing dp with 0 instead of INF: For minimization problems,
dp[w] = 0means "0 coins" which will never get improved. Usedp[w] = INFand onlydp[0] = 0. - Not checking
dp[w-c] != INFbefore using it:INF + 1overflows! Always check that the subproblem is solvable. - Wrong loop order for knapsack variants: For unbounded (unlimited coins), loop amounts forward. For 0/1 (each used once), loop amounts backward. Getting this wrong gives wrong answers silently.
- Using
INT_MAXas INF then adding 1:INT_MAX + 1overflows to negative. Use1e9or1e18as INF. - Forgetting the base case:
dp[0] = 0is crucial. Without it, nothing ever gets set.
Chapter Summary
๐ Key Takeaways
| Concept | Key Points | When to Use |
|---|---|---|
| Overlapping subproblems | Same computation repeated exponentially | Duplicate calls in recursion tree |
| Memoization (top-down) | Cache recursive results; easy to write | When recursive structure is clear |
| Tabulation (bottom-up) | Iterative table-filling; no stack overflow | Final contest solution; large N |
| DP state | Information that uniquely identifies a subproblem | Define carefully โ determines everything |
| DP recurrence | How dp[state] depends on smaller states | "Transition equation" |
| Base case | Known answer for the simplest subproblem | Usually dp[0] = some trivial value |
๐งฉ DP Four-Step Method Quick Reference
| Step | Question | Fibonacci Example |
|---|---|---|
| 1. Define state | "What does dp[i] represent?" | dp[i] = the i-th Fibonacci number |
| 2. Write recurrence | "Which smaller states does dp[i] depend on?" | dp[i] = dp[i-1] + dp[i-2] |
| 3. Determine base case | "What is the answer for the smallest subproblem?" | dp[0]=0, dp[1]=1 |
| 4. Determine fill order | "i from small to large? Large to small?" | i from 2 to n |
โ FAQ
Q1: How do I tell if a problem is a DP problem?
A: Two signals: โ the problem asks for an "optimal value" or "number of ways" (not "output the specific solution"); โก there are overlapping subproblems (the same subproblem is computed multiple times in brute-force recursion). If greedy can be proven correct, DP is usually not needed; otherwise it's likely DP.
Q2: Should I use top-down or bottom-up?
A: While learning, use top-down (more naturally expresses recursive thinking); for contest submission, use bottom-up (faster, no stack overflow). Both are correct. If you can quickly write bottom-up, go with it directly.
Q3: What is "optimal substructure" (no aftereffect)?
A: The core prerequisite of DP โ once
dp[i]is determined, subsequent computations will not "come back" to change it. In other words,dp[i]'s value only depends on the "past" (smaller states), not the "future". If this property is violated, DP cannot be used.
Q4: What value should INF be set to?
A: For
intuse1e9(= 10^9), forlong longuse1e18(= 10^18). Do not useINT_MAX, becauseINT_MAX + 1overflows to a negative number.
๐ Connections to Later Chapters
- Chapter 6.2 (Classic DP): extends to LIS, knapsack, grid paths โ all applications of the four-step DP method from this chapter
- Chapter 6.3 (Advanced DP): enters bitmask DP, interval DP, tree DP โ more complex state definitions but same thinking
- Chapter 3.2 (Prefix Sums): difference arrays can sometimes replace simple DP, and prefix sum arrays can speed up interval computations in DP
- Chapter 4.1 (Greedy) vs DP: greedy-solvable problems are a special case of DP (local optimum = global optimum at each step); when greedy fails, DP is needed
Practice Problems
Problem 6.1.1 โ Climbing Stairs ๐ข Easy
You can climb 1 or 2 stairs at a time. How many ways to climb N stairs?
(Same as Fibonacci โ ways[n] = ways[n-1] + ways[n-2])
Hint
This is exactly Fibonacci! ways[1]=1, ways[2]=2. Or start with ways[0]=1, ways[1]=1, then ways[n] = ways[n-1] + ways[n-2].โ Full Solution
Core Idea: ways[n] = number of ways to reach stair n. You arrived from stair n-1 (1-step) or stair n-2 (2-step). This gives the Fibonacci recurrence.
Input: Single integer N (1 โค N โค 45).
Output: Number of distinct ways.
Sample:
Input: 4
Output: 5
Explanation: [1+1+1+1, 1+1+2, 1+2+1, 2+1+1, 2+2]
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
if (n == 1) { cout << 1; return 0; }
vector<long long> dp(n + 1);
dp[1] = 1; dp[2] = 2;
for (int i = 3; i <= n; i++)
dp[i] = dp[i-1] + dp[i-2]; // come from n-1 or n-2
cout << dp[n] << "\n";
}
Trace for N=4:
dp[1]=1, dp[2]=2
dp[3] = dp[2]+dp[1] = 3
dp[4] = dp[3]+dp[2] = 5 โ
Complexity: O(N) time, O(N) space (reducible to O(1) with two variables).
Problem 6.1.2 โ Minimum Coin Change ๐ก Medium Given coin denominations [1, 3, 4] and target 6, find the minimum coins. (Expected answer: 2 coins โ use 3+3)
Hint
Build `dp[0..6]` using the coin change recurrence. Greedy gives 4+1+1=3 coins, but dp finds 3+3=2.โ Full Solution
Core Idea: dp[w] = minimum coins to make amount w exactly. For each amount, try all coins.
Input: First line: N (number of coins) and W (target). Second line: N coin values.
Output: Minimum coins, or -1 if impossible.
Sample:
Input:
3 6
1 3 4
Output: 2
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, W; cin >> n >> W;
vector<int> coins(n);
for (int& c : coins) cin >> c;
const int INF = 1e9;
vector<int> dp(W + 1, INF);
dp[0] = 0; // base: 0 coins to make amount 0
for (int w = 1; w <= W; w++) {
for (int c : coins) {
if (c <= w && dp[w - c] != INF)
dp[w] = min(dp[w], dp[w - c] + 1); // use coin c
}
}
cout << (dp[W] == INF ? -1 : dp[W]) << "\n";
}
Trace for coins=[1,3,4], W=6:
dp[0]=0, dp[1]=1(1), dp[2]=2(1+1), dp[3]=1(3)
dp[4]=1(4), dp[5]=2(1+4 or 1+4), dp[6]=2(3+3) โ
Greedy picks 4 first โ 4+1+1 = 3 coins. DP finds 3+3 = 2 coins.
Complexity: O(N ร W) time, O(W) space.
Problem 6.1.3 โ Tile Tiling ๐ก Medium A 2รN board can be tiled with 1ร2 dominoes (placed horizontally or vertically). How many ways?
Hint
Same recurrence as Fibonacci! The key insight: when you place a vertical domino at column n, you recurse on n-1; when you place two horizontal dominoes at columns n-1 and n, you recurse on n-2.โ Full Solution
Core Idea: Look at the rightmost column. Either:
- Place one vertical domino (fills column N alone) โ
dp[N-1]ways for the rest - Place two horizontal dominoes (fills columns N-1 and N) โ
dp[N-2]ways
So dp[N] = dp[N-1] + dp[N-2] โ exactly Fibonacci!
Input: Integer N (1 โค N โค 60).
Output: Number of tilings modulo 10โน+7.
Sample:
Input: 4
Output: 5
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1e9 + 7;
int main() {
int n; cin >> n;
if (n == 1) { cout << 1; return 0; }
vector<long long> dp(n + 1);
dp[1] = 1; dp[2] = 2;
for (int i = 3; i <= n; i++)
dp[i] = (dp[i-1] + dp[i-2]) % MOD;
cout << dp[n] << "\n";
}
Visual for N=4 (5 tilings):
|||| โ 4 vertical |=| โ V+2H+V =| โ 2H top + V
|=| =|
... (5 total)
Complexity: O(N) time, O(N) space.
Problem 6.1.4 โ Bounded Coin Change ๐ด Hard Same as coin change, but you can use each coin at most once (0/1 knapsack). Find the minimum coins.
Hint
This is a 0/1 knapsack variant. In the 1D space-optimized version, iterate w from W down to coins[i] to prevent reuse.โ Full Solution
Core Idea: 0/1 knapsack where each coin can be used at most once. The critical trick: iterate w backwards (Wโcoins[i]) to prevent reusing the same coin.
Input: First line: N W. Second line: N coin values.
Output: Minimum coins, or -1 if impossible.
Sample:
Input:
4 7
1 2 4 5
Output: 2
(use 2+5=7)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, W; cin >> n >> W;
vector<int> coins(n);
for (int& c : coins) cin >> c;
const int INF = 1e9;
vector<int> dp(W + 1, INF);
dp[0] = 0;
for (int i = 0; i < n; i++) {
// REVERSE order: prevents using coin i more than once
for (int w = W; w >= coins[i]; w--) {
if (dp[w - coins[i]] != INF)
dp[w] = min(dp[w], dp[w - coins[i]] + 1);
}
}
cout << (dp[W] == INF ? -1 : dp[W]) << "\n";
}
Why reverse? If we go forward, we might update dp[w] and then use that updated value later in the same pass โ effectively using coin i twice. Reverse order reads only "before coin i was considered" values.
Complexity: O(N ร W) time, O(W) space.
Problem 6.1.5 โ USACO Bronze: Haybale Stacking ๐ด Hard Given N operations "add 1 to all positions from L to R", determine the final value at each position.
Hint
Difference array: `diff[L]++`, `diff[R+1]--`. Then prefix sum of diff gives final values.โ Full Solution
Core Idea: Naive: O(NรQ) โ too slow for large inputs. Difference array technique: mark +1 at L and -1 at R+1, then take prefix sum. O(N+Q).
Input: N (array size), Q (operations). Then Q lines: L R.
Output: Final values A[1..N].
Sample:
Input:
5 3
1 3
2 4
3 5
Output:
1 2 3 2 1
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false); cin.tie(NULL);
int n, q; cin >> n >> q;
vector<long long> diff(n + 2, 0); // difference array, 1-indexed
while (q--) {
int l, r; cin >> l >> r;
diff[l]++; // start of range: +1
diff[r + 1]--; // after range: -1 (cancel)
}
// prefix sum of diff = actual values
long long cur = 0;
for (int i = 1; i <= n; i++) {
cur += diff[i];
cout << cur << " \n"[i == n];
}
}
Trace for sample:
After ops: diff = [0,1,1,1,-1,-1,-1,0] (1-indexed)
Prefix: A = [0,1,2, 3, 2, 1, 0,0]
Output: 1 2 3 2 1 โ
Complexity: O(N + Q) time, O(N) space.
๐ Challenge Problem: Unique Paths with Obstacles An NรM grid has '.' cells and '#' obstacles. Count paths from (1,1) to (N,M) moving only right or down. Answer modulo 10^9+7. (N, M โค 1000)
โ Full Solution
Core Idea: 2D DP. dp[i][j] = number of ways to reach cell (i,j). If (i,j) is blocked, dp[i][j]=0. Otherwise dp[i][j] = dp[i-1][j] + dp[i][j-1].
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1e9 + 7;
int main() {
int n, m; cin >> n >> m;
vector<string> grid(n);
for (auto& row : grid) cin >> row;
vector<vector<long long>> dp(n, vector<long long>(m, 0));
if (grid[0][0] == '.') dp[0][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == '#') { dp[i][j] = 0; continue; }
if (i > 0) dp[i][j] = (dp[i][j] + dp[i-1][j]) % MOD;
if (j > 0) dp[i][j] = (dp[i][j] + dp[i][j-1]) % MOD;
}
}
cout << dp[n-1][m-1] << "\n";
}
Complexity: O(N ร M) time and space.
Visual: Fibonacci Recursion Tree
The diagram shows naive recursion for fib(6). Red dashed nodes are duplicate subproblems โ computed multiple times. Green nodes show where memoization caches results. Without memoization: O(2^N). With memoization: O(N). This is the fundamental insight behind dynamic programming.