๐Ÿ“– Chapter 6.1 โฑ๏ธ ~65 min read ๐ŸŽฏ Intermediate

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:

  1. Overlapping subproblems โ€” the same sub-computation appears many times
  2. 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.

Fibonacci Memoization

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):

Memoization vs Tabulation

๐Ÿ’ก ๆ ธๅฟƒๅŒบๅˆซ๏ผš Top-Down ๆŒ‰้œ€่ฎก็ฎ—๏ผˆๅช็ฎ—็”จๅˆฐ็š„ๅญ้—ฎ้ข˜๏ผ‰๏ผŒBottom-Up ๅ…จ้‡ๅกซ่กจ๏ผˆๆŒ‰้กบๅบ็ฎ—ๆ‰€ๆœ‰ๅญ้—ฎ้ข˜๏ผ‰ใ€‚ไธค่€…ๆ—ถ้—ดๅคๆ‚ๅบฆ็›ธๅŒ๏ผŒไฝ† Bottom-Up ๆ— ้€’ๅฝ’ๆ ˆๅผ€้”€ใ€‚

AspectMemoization (Top-Down)Tabulation (Bottom-Up)
ApproachRecursive with cachingIterative table filling
Memory usageOnly computed statesAll states (even unused)
ImplementationOften more intuitiveMay need to figure out fill order
Stack overflow riskYes (deep recursion)No
SpeedSlightly slower (function call overhead)Slightly faster
Subproblems computedOnly reachable onesAll (even unreachable)
DebuggingEasier (follow recursion)Harder (need correct fill order)
USACO preferenceGreat for understandingGreat 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:

DP 4-Step Recipe

  1. Define the state: What information uniquely describes a subproblem?
  2. Define the recurrence: How does dp[state] depend on smaller states?
  3. Identify base cases: What are the simplest subproblems with known answers?
  4. Determine order: In what order should we fill the table?

Let's apply this to Fibonacci:

  1. State: dp[i] = the i-th Fibonacci number
  2. Recurrence: dp[i] = dp[i-1] + dp[i-2]
  3. Base cases: dp[0] = 0, dp[1] = 1
  4. 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.

Coin Change DP

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:

Coin Change State Transitions

๐Ÿ’ก Transition direction: Each dp[w] transitions from dp[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 amount w
  • 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

  1. Initializing dp with 0 instead of INF: For minimization problems, dp[w] = 0 means "0 coins" which will never get improved. Use dp[w] = INF and only dp[0] = 0.
  2. Not checking dp[w-c] != INF before using it: INF + 1 overflows! Always check that the subproblem is solvable.
  3. 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.
  4. Using INT_MAX as INF then adding 1: INT_MAX + 1 overflows to negative. Use 1e9 or 1e18 as INF.
  5. Forgetting the base case: dp[0] = 0 is crucial. Without it, nothing ever gets set.

Chapter Summary

๐Ÿ“Œ Key Takeaways

ConceptKey PointsWhen to Use
Overlapping subproblemsSame computation repeated exponentiallyDuplicate calls in recursion tree
Memoization (top-down)Cache recursive results; easy to writeWhen recursive structure is clear
Tabulation (bottom-up)Iterative table-filling; no stack overflowFinal contest solution; large N
DP stateInformation that uniquely identifies a subproblemDefine carefully โ€” determines everything
DP recurrenceHow dp[state] depends on smaller states"Transition equation"
Base caseKnown answer for the simplest subproblemUsually dp[0] = some trivial value

๐Ÿงฉ DP Four-Step Method Quick Reference

StepQuestionFibonacci 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 int use 1e9 (= 10^9), for long long use 1e18 (= 10^18). Do not use INT_MAX, because INT_MAX + 1 overflows 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

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.