Hello! Welcome to your next lesson on classic Dynamic Programming applications.
In our last session, we successfully tackled the 0/1 Knapsack problem. We saw how to define subproblems based on items and capacity, derive a recurrence relation, and build a table to find the maximum value. You also learned how to trace back through that table to find the items themselves and how to optimize the solution for space.
Today, we'll apply that same DP thinking to a different kind of problem: one that operates on sequences. Our goal is to solve the Longest Common Subsequence (LCS) problem and reconstruct the subsequence. This is another cornerstone DP problem with direct applications in tools you might use daily, like the diff utility for comparing file changes in version control systems like Git, and in the field of bioinformatics for DNA sequence analysis.
1. What is a Longest Common Subsequence?
First, let's clarify our terms. Given a sequence (like a string), a subsequence is formed by deleting zero or more characters from the original sequence, while keeping the remaining characters in their original relative order. This is different from a substring, which must be a contiguous block of characters.
For example, for the string ABCDEFG:
ACEGis a subsequence.CDis a subsequence and a substring.AGFis not a subsequence, because theFandGare out of order.
The Longest Common Subsequence (LCS) problem takes two sequences, X and Y, and asks for the longest subsequence that is common to both.
For example, if X = "ABCBDAB" and Y = "BDCABA", the longest common subsequence is BCBA, which has a length of 4. Note that BCAB is another valid LCS of the same length. The solution isn't always unique, but its length is.
Chapter 12 Dynamic Programming: The Longest Common Subsequence Problem
For a formal definition and to see why a brute-force approach of checking every subsequence is computationally infeasible, you can read the introduction of this chapter from a standard algorithms textbook.
Read the sections '12.5 The Longest Common Subsequence Problem' and '12.5.1 Problem Definition'. This will set the stage and highlight the need for a more efficient method like dynamic programming.
2. Formulating the DP Solution
Just like with the Knapsack problem, the key is to define a subproblem that has an optimal substructure. Since we are dealing with two input sequences, our subproblems will need to incorporate information about both.
A powerful way to think about this is to define subproblems on suffixes of the original strings. Let's say our strings are A and B. We can define a function L(i, j) that gives the length of the LCS for the suffixes A[i:] (from index i to the end) and B[j:] (from index j to the end).
To find L(i, j), we look at the first characters of our current suffixes, A[i] and B[j]:
-
If
A[i] == B[j]: The characters match! This character can be part of our LCS. The length of the LCS will be 1 (for this character) plus the length of the LCS of the rest of the suffixes. So, we have1 + L(i+1, j+1). -
If
A[i] != B[j]: The characters don't match. This means we cannot include bothA[i]andB[j]at the start of our common subsequence. We have to discard one of them.- If we discard
A[i], the problem becomes finding the LCS ofA[i+1:]andB[j:], which isL(i+1, j). - If we discard
B[j], the problem becomes finding the LCS ofA[i:]andB[j+1:], which isL(i, j+1).
Since we want the longest common subsequence, we take the maximum of these two possibilities.
- If we discard
This gives us the complete recurrence relation:
The base cases occur when either i or j goes past the end of the string, meaning we've reached an empty suffix. The LCS of anything and an empty string is 0.
16. Dynamic Programming, Part 2: LCS, LIS, Coins
The following video from MIT OpenCourseWare provides an excellent theoretical walkthrough of this exact formulation. The instructor's structured approach to defining subproblems and deriving the recurrence will resonate with your preference for understanding the 'why'.
Watch from 03:45 to 14:20. The video first defines the LCS problem, then introduces the idea of using suffixes for subproblems, and finally derives the recurrence relation we just discussed.
3. The Tabulation Approach
While the suffix-based recurrence is great for theoretical understanding, most practical implementations use a prefix-based approach. It's slightly more intuitive when filling a table from the top-left.
Let's define dp[i][j] as the length of the LCS of the prefixes A[0...i-1] and B[0...j-1]. The table will have len(A)+1 rows and len(B)+1 columns to account for empty prefixes.
The recurrence is very similar, just with indices decreasing instead of increasing:
- If
A[i-1] == B[j-1]:dp[i][j] = 1 + dp[i-1][j-1] - If
A[i-1] != B[j-1]:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
We fill the table starting from dp[0][0], and the final answer (the length of the LCS for the entire strings) will be in the bottom-right cell, dp[len(A)][len(B)].
Longest Common Subsequence Visually Explained | Dynamic Programming
To see this prefix-based table-filling in action, this short video from Hello Byte offers a clear, visual walkthrough.
Watch from 01:07 to 04:19. Observe how each cell is filled based on the two rules (match or no match) by looking at the cells above, to the left, and diagonally up-left.
For reference, here is the pseudocode that implements this logic. Notice how it mirrors the structure of the Knapsack tabulation algorithm, with two nested loops filling a 2D array.
Longest common subsequence - Wikipedia
The Wikipedia article on LCS provides clear and concise pseudocode for this process.
Review the section 'Computing the length of the LCS' to see the algorithm LCSLength.
4. Reconstructing the Subsequence
Just like with Knapsack, our DP table holds the key to finding not just the length, but the actual subsequence. We can reconstruct it by backtracking from the bottom-right cell (dp[len(A)][len(B)]).
Here is the logic for backtracking from cell (i, j):
- Compare the characters
A[i-1]andB[j-1]. - If they are equal, this character is part of the LCS. We prepend it to our result and move diagonally to cell
(i-1, j-1). - If they are not equal, we didn't find a match. We need to figure out which direction contributed to the maximum value in the current cell. We move to the larger of the two adjacent cells:
- If
dp[i-1][j] > dp[i][j-1], we move up to(i-1, j). - Otherwise, we move left to
(i, j-1). (If they're equal, either path works, leading to potentially different but equally long LCSs).
- If
- We repeat this process until we reach the first row or column of the table.

Longest Common Subsequence Visually Explained | Dynamic Programming
The Hello Byte video also provides a concise explanation of this backtracking process.
Watch the segment from 04:19 to 05:09. It directly follows the table-filling portion and demonstrates the reconstruction logic.
To see this as an algorithm, you can again refer to the Wikipedia resource.
Longest common subsequence - Wikipedia
The following section in the Wikipedia article provides pseudocode for a backtrack function.
Read the section 'Reading out a LCS'. This recursive function perfectly implements the backtracking logic we just described.
5. Complexity Analysis
Let the lengths of the two strings be m and n.
- Time Complexity: The algorithm is dominated by filling the DP table. Since we have a nested loop structure that iterates through each cell of the
(m+1) x (n+1)table once, and each computation is constant time, the overall time complexity is . - Space Complexity: We need to store the entire
(m+1) x (n+1)table to perform the backtracking. Therefore, the space complexity is .
As with the Knapsack problem, it is possible to optimize the space to if you only need the length of the LCS. However, this optimization makes reconstructing the actual subsequence more complex, so the space approach is standard when the subsequence itself is required.
Conclusion
Great work! You have now solved another classic DP problem. The Longest Common Subsequence problem demonstrates how dynamic programming can be applied to problems involving sequences by defining subproblems based on their prefixes or suffixes.
Key Takeaways:
- The LCS problem seeks the longest ordered sequence of characters common to two strings.
- The DP subproblem can be defined on prefixes (or suffixes) of the input strings:
dp[i][j]= LCS ofA[0...i-1]andB[0...j-1]. - The recurrence relation depends on whether the characters at the current positions match or not.
- A 2D table of size is filled to find the length of the LCS.
- The actual subsequence is found by backtracking from the end of the table, following the decisions made at each step.
Preview of the Next Lesson:
We will continue exploring DP on sequences by looking at the Longest Increasing Subsequence (LIS) problem. While it also deals with subsequences, it only involves a single input sequence and requires a cleverer subproblem definition than what we've seen so far. It will be a great test of your growing DP skills