Lesson 41 of 50 advanced

Dynamic Programming — Remember and Reuse

Learn the most powerful problem-solving technique — solve it once, remember it forever!

Open interactive version (quiz + challenge)

Real-world analogy

Imagine you are a student taking a math test, and the test keeps asking you the same addition problems over and over. The first time you see 7 + 8, you calculate it carefully and get 15. But instead of throwing away your work, you write the answer in a notebook. The next time 7 + 8 appears, you just look it up — instant answer! That is exactly what Dynamic Programming does. It solves each small problem once, writes down the answer, and reuses it whenever that same problem comes up again. No wasted effort!

What is it?

Dynamic Programming (DP) is an algorithmic technique that solves complex problems by breaking them into simpler overlapping subproblems, solving each subproblem just once, and storing the results for future use. It transforms exponentially slow recursive solutions into efficient polynomial-time ones.

Real-world relevance

DP is used everywhere in the real world! Google Maps uses DP to find the shortest route between cities. Spell checkers use DP (edit distance) to suggest corrections for misspelled words. Netflix uses DP-like algorithms to recommend movies you might enjoy. Even your phone's autocorrect uses Dynamic Programming to figure out what word you meant to type!

Key points

Code example

#include <bits/stdc++.h>
using namespace std;

// ===== METHOD 1: Top-Down (Memoization) =====
long long memo[51];  // Store computed Fibonacci values
bool computed[51];   // Track which values are computed

long long fibMemo(int n) {
    if (n <= 1) return n;           // Base cases: fib(0)=0, fib(1)=1
    if (computed[n]) return memo[n]; // Already solved? Return stored answer!
    
    memo[n] = fibMemo(n - 1) + fibMemo(n - 2);  // Solve and store
    computed[n] = true;
    return memo[n];
}

// ===== METHOD 2: Bottom-Up (Tabulation) =====
long long fibTab(int n) {
    if (n <= 1) return n;
    
    vector<long long> dp(n + 1);
    dp[0] = 0;  // Base case
    dp[1] = 1;  // Base case
    
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];  // Build from bottom up!
    }
    return dp[n];
}

// ===== METHOD 3: Space-Optimized Bottom-Up =====
long long fibOptimized(int n) {
    if (n <= 1) return n;
    
    long long prev2 = 0, prev1 = 1;
    for (int i = 2; i <= n; i++) {
        long long curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;  // Only need last two values!
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    
    int n;
    cout << "Enter n to find Fibonacci(n): ";
    cin >> n;
    
    cout << "Memoization: fib(" << n << ") = " << fibMemo(n) << "\n";
    cout << "Tabulation:  fib(" << n << ") = " << fibTab(n) << "\n";
    cout << "Optimized:   fib(" << n << ") = " << fibOptimized(n) << "\n";
    
    return 0;
}

Line-by-line walkthrough

  1. 1. We create two global arrays: memo[] to store Fibonacci values and computed[] to track which ones we have already calculated. These are our 'sticky notes' for the top-down approach.
  2. 2. In fibMemo(n), the base case returns n directly when n is 0 or 1. This is because fib(0) = 0 and fib(1) = 1 — the starting points of the sequence.
  3. 3. Before computing, we check if computed[n] is true. If it is, we already solved this subproblem! We just return memo[n] instantly. This is the magic of memoization — no repeated work.
  4. 4. If not yet computed, we recursively call fibMemo(n-1) + fibMemo(n-2), store the result in memo[n], mark computed[n] as true, and return the answer.
  5. 5. In fibTab(n), we create a dp vector and fill it from the bottom up. dp[0] = 0 and dp[1] = 1 are our base cases. Then we loop from 2 to n, filling each dp[i] = dp[i-1] + dp[i-2].
  6. 6. The space-optimized version fibOptimized(n) is clever — since we only ever need the last two values, we use just two variables (prev1 and prev2) instead of a whole array. This saves memory!
  7. 7. In each iteration of the optimized loop, we compute curr = prev1 + prev2, then shift: prev2 becomes prev1, and prev1 becomes curr. Like a caterpillar inching forward.
  8. 8. In main(), we read n and call all three methods. They all give the same answer, but use different approaches. The optimized version uses O(1) space — just two variables!
  9. 9. All three methods run in O(n) time, which is incredibly fast compared to the naive recursive O(2^n). For n=50, that is the difference between instant and waiting longer than the age of the universe!

Spot the bug

#include <bits/stdc++.h>
using namespace std;

long long memo[101];

long long fib(int n) {
    if (n <= 1) return n;
    if (memo[n] != 0) return memo[n];
    
    memo[n] = fib(n - 1) + fib(n - 2);
    return memo[n];
}

int main() {
    cout << fib(0) << "\n";  // Should print 0
    cout << fib(1) << "\n";  // Should print 1
    cout << fib(2) << "\n";  // Should print 1
    cout << fib(10) << "\n"; // Should print 55
    return 0;
}
Need a hint?
Think about what value fib(0) returns and what the memo check condition is. Is checking memo[n] != 0 always correct for memoization?
Show answer
The bug is using memo[n] != 0 to check if a value is computed. The problem is that fib(0) = 0, so memo[0] will always be 0, and the check thinks it has not been computed yet! This causes infinite recursion for some cases. The fix is to initialize memo with -1 (using memset(memo, -1, sizeof(memo))) and check memo[n] != -1 instead. This way, -1 means 'not yet computed' and 0 is a valid computed answer.

Explain like I'm 5

Imagine you are coloring a picture and your friend keeps asking you 'what color is the sky?' You tell them 'blue' the first time. But if they ask again and again, you would get tired of answering! So you write 'SKY = BLUE' on a sticky note and put it on the wall. Now whenever anyone asks, you just point to the sticky note. Dynamic Programming is like putting sticky notes everywhere so you never have to figure out the same thing twice!

Fun fact

The term 'Dynamic Programming' was invented by Richard Bellman in the 1950s. He chose the word 'dynamic' because it sounded cool and impressive — not because it describes what the technique does! He wanted to hide the mathematical nature of his work from a secretary of defense who hated math research. So the name is basically a marketing trick that stuck forever!

Hands-on challenge

Implement both memoization and tabulation versions of Fibonacci. Then try this: compute the number of ways to tile a 2xN rectangle using 2x1 tiles. Hint: the answer for a 2xN rectangle follows the Fibonacci pattern! Can you figure out why? Write both a top-down and bottom-up solution. Test with N=1 through N=10 and verify your answers: 1, 2, 3, 5, 8, 13, 21, 34, 55, 89.

More resources

Open interactive version (quiz + challenge) ← Back to course: CP Zero to Hero