Hello! Welcome back to our module on Dynamic Programming.
In our last lesson, we mastered the 0/1 Knapsack problem. We saw how to define a recurrence relation, implement it with both top-down (memoization) and bottom-up (tabulation) approaches, analyze its complexity, and even optimize its space usage. These are the core skills of dynamic programming.
Today, we will apply this exact same thinking process to a new challenge: the Longest Common Subsequence (LCS) problem. This lesson will show you how versatile the DP paradigm is. By the end, you will be able to solve the LCS problem by identifying its subproblems, formulating a recurrence, and implementing an efficient DP solution. This is a classic problem often used in version control systems (like git diff) and bioinformatics, making it a valuable tool in your algorithmic toolkit.
1. What is a Subsequence?
First, let's clarify the terminology. A subsequence is a sequence that can be derived from another sequence by deleting zero or more elements without changing the order of the remaining elements. For example, "ACE" is a subsequence of "ABCDE", but "AEC" is not.
The Longest Common Subsequence problem asks for the length of the longest subsequence that is common to two given strings.
Longest Common Subsequence (Dynamic Programming)
Let's start with a formal definition of the problem and the core recursive logic we'll use to solve it. This video from CS Dojo clearly explains the problem and breaks it down into manageable subproblems.
Watch the segments from 00:00 to 00:40 and 01:04 to 04:54. The first part defines the problem with an example. The second, more crucial part, explains the two cases for the recursive solution: when the last characters match and when they don't. This recurrence relation is the heart of our DP solution.
2. The Recurrence Relation
As the video explained, we can define the problem recursively. Let LCS(X, Y) be the length of the longest common subsequence of strings X and Y. Let X_i be the prefix of X of length i, and Y_j be the prefix of Y of length j.
We can define dp[i][j] as the length of the LCS of X_i and Y_j. To find dp[i][j], we look at the last characters, X[i-1] and Y[j-1]:
-
If the characters match (
X[i-1] == Y[j-1]):
The LCS must include this character. The length is 1 plus the LCS of the strings without these last characters. -
If the characters do not match (
X[i-1] != Y[j-1]):
The LCS does not include both characters. We must discard one of them. We find the LCS by either discarding the last character ofXor the last character ofY, and we take the longer of the two results.
These two rules, along with base cases dp[i][0] = 0 and dp[0][j] = 0 (LCS of any string with an empty string is 0), give us everything we need to solve the problem.
3. From Inefficient Recursion to Efficient DP
A naive recursive implementation of this logic would be incredibly slow, with a time complexity of , because it repeatedly solves the same subproblems. This is the same issue we saw with the 0/1 Knapsack problem.
The solution, once again, is dynamic programming.
Memoization (Top-Down)
We can use our recursive function but add a 2D array (our memoization table) to cache the results of subproblems. Before computing LCS(X_i, Y_j), we check if the result is already in our table. If so, we return it; otherwise, we compute it, store it, and then return it.
Longest Common Subsequence (Dynamic Programming)
This next segment illustrates why the naive recursion is slow and how memoization elegantly fixes it. The visualization of the recursion tree with overlapping subproblems should look very familiar.
Continue watching from 04:54 to 08:06. Notice how the recursion tree shows duplicate computations (e.g., L(1,2) is computed twice). Then, see how adding a cache transforms the exponential solution into an efficient one.
Tabulation (Bottom-Up)
Alternatively, we can solve this iteratively. We create a 2D table of size (m+1) x (n+1) and fill it out starting from the base cases dp[0][0]. Each cell dp[i][j] is filled using the same recurrence relation, relying on values that have already been computed (dp[i-1][j], dp[i][j-1], and dp[i-1][j-1]).
Longest Common Subsequence (2 Strings) - Dynamic Programming & Competing Subproblems
The tabulation method can be very intuitive. This video from Back To Back SWE gives a great walkthrough of filling the DP table, clearly linking each cell to the subproblem it represents.
Watch from 13:13 to 23:59. Follow along as the table is filled for a detailed example. This visual process will help solidify your understanding of how the final solution is built from the ground up.
4. Implementation and Complexity
Now that you've seen the logic for both the top-down and bottom-up approaches, let's look at their implementation and formal complexity analysis.
This article provides clean pseudocode and code examples for both the memoized and tabulated solutions, along with a concise complexity analysis.
Read the sections 'Using top-down approach of dynamic programming' and 'Using bottom-up approach of dynamic programming'. For each section, review the solution idea, the code, and the 'Time and space complexity analysis'. Notice that both approaches yield the same complexity.
As the article confirms, both DP approaches share the same complexity:
- Time Complexity: , where
mandnare the lengths of the two strings. We need to compute the value for each of them x ncells in our DP table, and each computation takes constant time. - Space Complexity: to store the DP table or the memoization cache.
Test your understanding!
Given two strings, X = "STONE" and Y = "LONGEST". You are building a DP table dp[i][j] to find the LCS length. What is the value of dp[3][4]?
(dp[3][4] corresponds to the LCS of prefixes STO and LONG)
Show answer
We need to find the LCS of X_3 = "STO" and Y_4 = "LONG".
The last characters are O and G, which do not match.
So, we use the rule: dp[3][4] = max(dp[2][4], dp[3][3]).
dp[2][4]is the LCS of"ST"and"LONG". The LCS is"L", so the length is 1. (You can work this out, or trust me for now).dp[3][3]is the LCS of"STO"and"LON". The LCS is"O", so the length is 1.
Therefore, dp[3][4] = max(1, 1) = 1. The LCS is "O".
5. Reconstructing the Actual Subsequence
Our DP table gives us the length of the LCS, but what if we want the subsequence itself? We can find it by backtracking through the filled table, starting from the last cell dp[m][n].
The logic is as follows:
- Start at
i = m,j = n. - If
X[i-1] == Y[j-1], this character is part of the LCS. Prepend it to your result string and move diagonally up-left to(i-1, j-1). - If the characters don't match, compare
dp[i-1][j]anddp[i][j-1].- If
dp[i-1][j] > dp[i][j-1], move up to(i-1, j). - Otherwise, move left to
(i, j-1). (If they are equal, you can choose either path; this means there are multiple longest common subsequences).
- If
- Repeat until
iorjbecomes 0.

6. Space Optimization
From an architectural perspective, it's always worth asking if we can optimize resource usage. To compute the values for the current row i of our DP table, we only need the values from the previous row, i-1. This means we don't need to store the entire m x n table in memory.
Just as with the Knapsack problem, we can optimize the space complexity. This is a common pattern in DP problems and a great example of the trade-offs you make as an engineer.
Read the sections 'Space-optimized solution of bottom-up approach' and 'Efficient Space-Optimized Solution'. This demonstrates how to reduce the space complexity from O(m*n) to O(n). Note the trade-off: this optimization makes it much more difficult to reconstruct the actual subsequence string.
By using only one or two arrays of size n, we can reduce the space complexity to (where n is the length of the shorter string), which is a significant improvement when dealing with very long strings.
Conclusion
Excellent work! You have successfully applied the dynamic programming framework to a second major problem. The thought process—identifying optimal substructure and overlapping subproblems, defining a recurrence, and then using memoization or tabulation—is a powerful and repeatable pattern.
Key Takeaways:
- The LCS problem seeks the longest ordered set of characters common to two strings.
- The core recurrence relation depends on whether the last characters of the prefixes match.
- Both top-down (memoization) and bottom-up (tabulation) DP approaches solve LCS in time and space.
- The actual subsequence string can be found by backtracking through the completed DP table.
- Space complexity can be optimized to by recognizing that only the previous row's results are needed for the current computation.
Preview of the Next Lesson:
In the next lesson, we will tackle the Edit Distance problem. You will be amazed at how similar it is to LCS. By making a small tweak to the recurrence relation we learned today, we can solve an entirely new problem, further cementing your understanding of the power and flexibility of dynamic programming.