LearnFrenzy
Sign in Take a free test

DSA and Competitive Programming · Lesson 30 of 33

Dynamic Programming in C++

6 min read Updated Nov 7, 2026 Saurabh Samir

Dynamic programming (DP) solves problems made of overlapping sub-problems by solving each sub-problem once and storing its answer. It turns exponential brute force into polynomial time, and it's the most feared – and most rewarded – topic in coding interviews. The trick is a repeatable method, shown here with the STL's vectors.

In this lesson

  • Memoization (top-down) vs tabulation (bottom-up)
  • A method for designing DP solutions
  • Coin change and 0/1 knapsack
  • Longest increasing subsequence in O(n log n), and modular arithmetic

From recursion to DP

#include <iostream>
#include <vector>

long long calls = 0;
long long fib_naive(int n) { calls++; return n < 2 ? n : fib_naive(n - 1) + fib_naive(n - 2); }

std::vector<long long> memo(91, -1);
long long fib_memo(int n) {                     // top-down: recursion + cache
    if (n < 2) return n;
    if (memo[n] != -1) return memo[n];
    return memo[n] = fib_memo(n - 1) + fib_memo(n - 2);
}

long long fib_table(int n) {                    // bottom-up: fill a table in order
    std::vector<long long> dp(n + 1);
    dp[0] = 0;
    if (n > 0) dp[1] = 1;
    for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
    return dp[n];
}

int main() {
    std::cout << fib_naive(30) << " using " << calls << " calls\n";
    std::cout << fib_memo(90) << '\n' << fib_table(90) << '\n';
    return 0;
}

Output

832040 using 2692537 calls
2880067194370816120
2880067194370816120

Designing a DP solution

  1. Statewhat does dp[i] (or dp[i][j]) mean? e.g. "fewest coins to make amount i"
  2. Transitionhow is a state built from smaller ones?
  3. Base casesthe smallest states you know directly
  4. Orderfill states so dependencies are ready (or memoise)
  5. Answerwhich state holds the final result?

Coin change

#include <algorithm>
#include <climits>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> coins{1, 5, 6, 9};
    int amount = 11;
    std::vector<int> dp(amount + 1, INT_MAX);         // dp[a] = fewest coins to make a
    dp[0] = 0;
    for (int a = 1; a <= amount; a++)
        for (int c : coins)
            if (c <= a && dp[a - c] != INT_MAX) dp[a] = std::min(dp[a], dp[a - c] + 1);
    std::cout << "fewest coins for " << amount << ": " << dp[amount] << '\n';

    std::vector<long long> ways(amount + 1, 0);       // number of ways (order doesn't matter)
    ways[0] = 1;
    for (int c : coins)                               // coins outer: each combination counted once
        for (int a = c; a <= amount; a++) ways[a] += ways[a - c];
    std::cout << "ways to make " << amount << ": " << ways[amount] << '\n';
    return 0;
}

Output

fewest coins for 11: 2
ways to make 11: 6

A greedy "take the biggest coin" approach gives 9 + 1 + 1 = 3 coins for 11, but DP finds 5 + 6 = 2. That's why DP is needed when greedy choices can be wrong.

0/1 knapsack

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> weight{1, 3, 4, 5}, value{15, 50, 60, 90};
    int capacity = 8;
    std::vector<int> dp(capacity + 1, 0);               // dp[w] = best value with capacity w
    for (std::size_t i = 0; i < weight.size(); i++)
        for (int w = capacity; w >= weight[i]; w--)       // backwards: each item used at most once
            dp[w] = std::max(dp[w], dp[w - weight[i]] + value[i]);
    std::cout << "best value: " << dp[capacity] << '\n';
    return 0;
}

Output

best value: 140

The 2D version dp[i][w] ("best value using the first i items and capacity w") is easier to read; looping capacity backwards lets one 1D array do the same job in O(W) memory.

Longest increasing subsequence with lower_bound

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> a{10, 9, 2, 5, 3, 7, 101, 18, 4, 20};
    std::vector<int> tails;                       // tails[k] = smallest tail of an increasing run of length k+1
    for (int x : a) {
        auto it = std::lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end()) tails.push_back(x);   // extends the longest run
        else *it = x;                                // a better (smaller) tail for that length
    }
    std::cout << "LIS length: " << tails.size() << '\n';
    return 0;
}

Output

LIS length: 5

The classic DP for LIS is O(n²); keeping the smallest possible tail for each length and binary-searching with lower_bound makes it O(n log n) – a favourite interview follow-up.

Modular arithmetic

Counting problems often say "print the answer modulo 10⁹ + 7", because the true count is astronomically large:

#include <iostream>
#include <vector>

constexpr long long MOD = 1'000'000'007;

long long power_mod(long long base, long long exp) {
    long long result = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp & 1) result = result * base % MOD;
        base = base * base % MOD;
        exp >>= 1;
    }
    return result;
}

int main() {
    int n = 1000;                               // ways to climb n stairs taking 1 or 2 steps
    std::vector<long long> dp(n + 1);
    dp[0] = dp[1] = 1;
    for (int i = 2; i <= n; i++) dp[i] = (dp[i - 1] + dp[i - 2]) % MOD;   // reduce at every step
    std::cout << dp[n] << '\n';
    std::cout << power_mod(2, 1'000'000) << '\n';
    return 0;
}

Output

107579939
235042059

Modular rules

Apply % after every addition and multiplication so values never overflow long long (two numbers below 10⁹ + 7 multiply to less than 9.2 × 10¹⁸). For subtraction, add MOD before taking % to stay non-negative: (a - b + MOD) % MOD.

Practice

Try it yourself

  1. Count the paths from the top-left to the bottom-right of a 3 × 7 grid, moving only right or down.

    Show solution
    #include <iostream>
    #include <vector>
    
    int main() {
        int rows = 3, cols = 7;
        std::vector<std::vector<long long>> dp(rows, std::vector<long long>(cols, 1));
        for (int r = 1; r < rows; r++)
            for (int c = 1; c < cols; c++) dp[r][c] = dp[r - 1][c] + dp[r][c - 1];
        std::cout << dp[rows - 1][cols - 1] << " paths\n";
        return 0;
    }

    Output

    28 paths

Quick check

Memoization vs tabulation?

Memoization caches results of a recursive solution (top-down); tabulation fills a table iteratively (bottom-up).

Why loop capacity backwards in the 1D knapsack?

So each item is used at most once – forwards would reuse it.

Why reduce modulo after every operation?

To keep numbers small enough not to overflow.

Summary

  • DP = state + transition + base cases + order; solve each sub-problem once.
  • Coin change, knapsack and grid paths are the classic templates; LIS improves to O(n log n) with lower_bound.
  • Reduce modulo 10⁹ + 7 at every step for large counts.

Interview practice: dynamic programming questions.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular