Skip to Content
Course content

214: Solving Dynamic Programming Problems in C++

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing code for a junior dev who was building a resource allocator for a cloud-scheduling tool. He had written a recursive function to find the most efficient way to pack tasks into server slots. It was elegant, clean, and absolutely useless in production. It worked perfectly for five tasks. When he tried it with twenty, the program basically froze, and his CPU usage spiked to 100% as the machine spent an eternity recalculating the exact same sub-problems millions of times. He had fallen into the classic trap of exponential time complexity because he wasn't remembering his results.

That's the core of Dynamic Programming (DP). It sounds intimidating—like something only PhDs do—but in reality, DP is just recursion with a memory bank. If you can break a problem down into smaller, overlapping sub-problems, you can store the result of each sub-problem the first time you solve it. Next time you need that answer, you just look it up. In C++, we usually do this using either a top-down approach (memoization) or a bottom-up approach (tabulation).

Caching Results with Top-Down Memoization

Top-down is the most intuitive way to start because it mirrors the natural recursive structure of the problem. You write your recursive function as usual, but before you return a value, you save it in a data structure—typically a std::vector or a std::unordered_map. Before the function does any heavy lifting, it checks that structure to see if the answer already exists.

Take the "Minimum Coin Change" problem. If you need to make 11 cents using coins of 1, 2, and 5, you might first ask: "What's the minimum coins for 10 cents, 9 cents, and 6 cents?" Each of those questions will, in turn, ask about 8 cents, 7 cents, and so on. Without memoization, you'll calculate the minimum coins for 1 cent thousands of times. Here is how we handle that in C++:

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

int solve(int amount, const std::vector<int>& coins, std::vector<int>& memo) {
    if (amount == 0) return 0;
    if (amount < 0) return -1;
    if (memo[amount] != -2) return memo[amount]; // -2 is our "unvisited" flag

    int minCoins = 1e9; // Use a large number as infinity
    for (int coin : coins) {
        int res = solve(amount - coin, coins, memo);
        if (res >= 0) minCoins = std::min(minCoins, res + 1);
    }

    return memo[amount] = (minCoins == 1e9) ? -1 : minCoins;
}

int main() {
    std::vector<int> coins = {1, 2, 5};
    int target = 11;
    std::vector<int> memo(target + 1, -2); 
    std::cout << "Min coins: " << solve(target, coins, memo) << std::endl;
}

I prefer std::vector over std::unordered_map for the cache whenever the input range is known and contiguous. The cache locality of a vector is significantly better, which matters when you're doing millions of lookups.

Building the Solution Bottom-Up

While memoization is easier to write, "tabulation" is often faster and avoids the risk of a stack overflow from deep recursion. Instead of starting at the target and working backward, you start at the simplest possible case (usually 0) and fill a table iteratively. You're essentially building a foundation and layering the solution on top of it.

In the coin change example, you'd create a vector of size target + 1 and iterate from 1 up to the target. For every value, you look back at previously computed values in the table to determine the current optimal answer. This turns the problem into a series of simple loops, which the C++ compiler can optimize much more aggressively than recursive calls.

#include <vector>
#include <algorithm>

int coinChangeTabulated(int amount, const std::vector<int>& coins) {
    // Initialize table with amount + 1 (which is effectively infinity)
    std::vector<int> dp(amount + 1, amount + 1);
    dp[0] = 0;

    for (int i = 1; i <= amount; ++i) {
        for (int coin : coins) {
            if (i - coin >= 0) {
                dp[i] = std::min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    return (dp[amount] > amount) ? -1 : dp[amount];
}

Managing Memory and Complexity in C++

When you're implementing these in a real-world system, be mindful of your space complexity. A 2D DP table (common in problems like Longest Common Subsequence) can eat up RAM quickly if the input strings are long. I've seen developers accidentally allocate a 100,000 x 100,000 int array on the stack, which is a guaranteed crash. Always use the heap via std::vector for large tables.

Furthermore, look for "space optimization" opportunities. In many bottom-up problems, you only ever need the results from the previous row or the last few indices. If you notice that dp[i] only depends on dp[i-1] and dp[i-2], you don't need a full vector; you just need two or three variables. This can reduce your space complexity from O(N) to O(1), which is a huge win for cache performance.




📋 Practical Task

Implementing a Version-String Similarity Tool using LCS

In software versioning, it's often useful to find the Longest Common Subsequence (LCS) between two version strings (e.g., "1.2.3.4" and "1.2.5.4") to determine how much of the versioning path is shared. This is a classic DP problem.

Your Task: Write a C++ program that takes two strings as input and returns the length of their Longest Common Subsequence. You must implement this using a bottom-up tabulation approach (a 2D std::vector) to ensure it can handle strings up to 1,000 characters without crashing the stack.

Requirements:

  • Create a function int computeLCS(const std::string& s1, const std::string& s2).
  • Use a 2D std::vector<std::vector<int>> to store the lengths of common subsequences.
  • The logic should be: if characters match, increment the value from the diagonal cell; if they don't, take the maximum of the cell above or the cell to the left.
  • Test your code with the strings "1.2.3.4" and "1.2.5.4" (the result should be 5, as "1.2..4" is the common subsequence).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.