C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
168: Dynamic Programming Patterns in C++
I've always found that the term "Dynamic Programming" sounds far more intimidating than it actually is. In reality, it's just a fancy way of saying, "I'm tired of calculating the same thing twice, so I'm going to write the answer down."
Let's look at a classic problem: the Longest Common Subsequence (LCS). Given two strings, we want to find the length of the longest sequence of characters that appear in both, in the same relative order (but not necessarily consecutively). For example, "ABCDE" and "ACE" have an LCS of "ACE", length 3.
The Recursive Trap
My first instinct is usually to write a recursive function. It's the most natural way to express the logic: if the last characters match, we count it and move back. If they don't, we try skipping a character from either string and see which path yields a better result. Here is how that looks in C++:
int lcs(std::string s1, std::string s2, int m, int n) {
if (m == 0 || n == 0) return 0;
if (s1[m-1] == s2[n-1]) {
return 1 + lcs(s1, s2, m-1, n-1);
} else {
return std::max(lcs(s1, s2, m, n-1), lcs(s1, s2, m-1, n));
}
}
This feels elegant, right? But let's try running this with strings of length 20 or 30. Suddenly, the program hangs. Why? Because we're creating a massive recursion tree. If the characters don't match, we branch twice. For strings of length $N$ and $M$, we're looking at an exponential time complexity of $O(2^{N+M})$. I'm effectively recalculating the LCS for the same suffixes thousands of times.
Plugging the Leaks with Memoization
I don't want to throw away the recursive logic—it's intuitive. Instead, I'll add a "notebook" to store results we've already found. In C++, a 2D vector is perfect for this. I'll initialize it with -1 to indicate that a specific sub-problem hasn't been solved yet.
int memo[101][101]; // Assuming max length 100
int lcs_memo(std::string& s1, std::string& s2, int m, int n) {
if (m == 0 || n == 0) return 0;
// Check if we already have the answer
if (memo[m][n] != -1) return memo[m][n];
if (s1[m-1] == s2[n-1]) {
return memo[m][n] = 1 + lcs_memo(s1, s2, m-1, n-1);
} else {
return memo[m][n] = std::max(lcs_memo(s1, s2, m, n-1),
lcs_memo(s1, s2, m-1, n));
}
}
Now, the performance is night and day. We've shifted from exponential time to $O(M \times N)$. This is "Top-Down" DP. We start with the big problem and break it down, caching as we go. It's great, but it still carries the overhead of the function call stack. If our strings were thousands of characters long, we'd hit a stack overflow.
Turning the Table
To avoid the stack entirely, I can flip the logic. Instead of starting at the end and going backward, I'll start at the beginning and build a table upward. This is "Bottom-Up" DP (Tabulation).
I need a 2D table where dp[i][j] represents the LCS of s1[0...i-1] and s2[0...j-1]. I'll fill it row by row. The logic remains the same, but now it's just nested loops.
int lcs_tabulated(std::string s1, std::string s2) {
int m = s1.size();
int n = s2.size();
std::vector> dp(m + 1, std::vector(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1[i-1] == s2[j-1]) {
dp[i][j] = 1 + dp[i-1][j-1];
} else {
dp[i][j] = std::max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
Notice what happened here. The "pattern" of DP is essentially identifying the dependency. To calculate dp[i][j], I only ever need the cell to the left, the cell above, and the cell diagonally up-left. This realization is powerful because it tells me I don't actually need the whole 2D table—I only need the current row and the previous row. If I were optimizing for memory in a production system, I'd swap that vector> for two simple vector arrays.
📋 Practical Task
Implement a Levenshtein Distance Calculator
The Levenshtein Distance (or Edit Distance) is a close cousin of the LCS problem. It measures how many single-character edits (insertions, deletions, or substitutions) are required to change one word into another. This is the core logic behind most "Did you mean...?" spell-checkers.
Your Task: Write a C++ function int editDistance(std::string s1, std::string s2) using a bottom-up DP approach (tabulation).
The Logic Rules:
- If the characters at the current position match, the cost is 0 (just move to the next characters).
- If they don't match, you must take the minimum of three possible operations:
- Insert:
1 + dp[i][j-1] - Delete:
1 + dp[i-1][j] - Substitute:
1 + dp[i-1][j-1]
- Insert:
- Base cases: If one string is empty, the distance is the length of the other string (all insertions or all deletions).
Test your implementation with "kitten" and "sitting" (the result should be 3: substitute 'k' for 's', substitute 'e' for 'i', and add 'g' at the end).
There are no comments for now.