Hello! Welcome to our next lesson on Dynamic Programming.
In the previous lesson, we tackled the 0/1 Knapsack problem. We saw how to use a 2D DP table to store optimal solutions to subproblems, where the state was defined by the item index and the remaining knapsack capacity. We also learned how to backtrack through that table to determine which items were included in the optimal set.
Today, we'll explore another classic problem that builds directly on those concepts: The Longest Common Subsequence (LCS). Your learning outcome for this lesson is to find the length of the Longest Common Subsequence and reconstruct it. This problem is a staple in technical interviews, and mastering it will solidify your understanding of 2D DP on strings.
1. What is a Longest Common Subsequence?
First, let's be clear on 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. This is different from a substring, which must be a contiguous block of characters.
For example, for the string "abcde":
- "ace" is a subsequence.
- "abd" is a subsequence.
- "bce" is a subsequence.
- "bcd" is a substring and a subsequence.
- "aec" is not a subsequence because the order of 'a' and 'e' is changed relative to 'c'.
A common subsequence is a subsequence that is present in two different strings. The Longest Common Subsequence (LCS) is the common subsequence with the greatest possible length.
Let's watch a short video to see this explained with clear examples.
Dp 25. Longest Common Subsequence | Top Down | Bottom-Up | Space Optimised | DP on Strings
This video from the 'take U forward' channel provides a great definition of a subsequence and the LCS problem.
Watch from 00:38 to 04:21. Focus on understanding the difference between a subsequence and a substring, and how the LCS is defined.
A brute-force approach would involve generating all subsequences of one string (which is an exponential number, ) and checking if they are present in the second string. This is highly inefficient. As you might guess, Dynamic Programming offers a much better way.
2. Finding the Length of the LCS
Let's develop the DP solution, starting with the recurrence relation. This logic is the heart of the problem.
Imagine we have two strings, s1 and s2, of lengths n and m respectively. We want to find the LCS of these two strings. We can define a function, lcs(i, j), that returns the length of the LCS for the prefixes s1[0...i] and s2[0...j].
Now, consider the last characters of these prefixes, s1[i] and s2[j]:
-
If the characters match (
s1[i] == s2[j]):
Great! This matching character is part of our LCS. We can add 1 to our count and then solve the smaller subproblem for the strings without these last characters.
The length is1 + lcs(i-1, j-1). -
If the characters do not match (
s1[i] != s2[j]):
We cannot use this pair of characters. Our LCS must be formed from a smaller set of characters. We have two choices:- Either the LCS is the same as the LCS of
s1[0...i-1]ands2[0...j](we discards1[i]). - Or it's the same as the LCS of
s1[0...i]ands2[0...j-1](we discards2[j]).
Since we want the longest common subsequence, we take the maximum of these two possibilities.
The length ismax(lcs(i-1, j), lcs(i, j-1)).
- Either the LCS is the same as the LCS of
This recurrence relation has overlapping subproblems, making it a perfect candidate for DP. The following video walks through this derivation in detail and shows how to convert it into both a memoized (top-down) and tabulated (bottom-up) solution.
Dp 25. Longest Common Subsequence | Top Down | Bottom-Up | Space Optimised | DP on Strings
Let's watch a more extensive segment of the 'take U forward' video. It brilliantly explains how to build the recurrence relation, handle base cases, and then translate it into a tabulated DP solution.
Watch from 06:12 to 14:30 to understand the recurrence relation. Then, skip to 33:19 and watch until 41:20 to see how this is converted into a bottom-up (tabulation) solution. Pay close attention to the 'shifting of index' technique, which simplifies the code by making the DP table 1-indexed.
Tabulation Implementation
As the video explained, the tabulated approach is often preferred for its efficiency (no recursion overhead). We create a 2D array, dp[n+1][m+1], where dp[i][j] will store the length of the LCS of s1.substring(0, i) and s2.substring(0, j).
Here is a clean Java implementation of the tabulation logic.
Dynamic Programming in Java with LCS
This article from Medium provides a concise Java implementation of the concepts we just saw. Focus on the bottom-up approach section.
Read the section 'Building the LCS Table'. Compare the nested loops and the if/else logic in the provided Java code to the recurrence relation we just established. Notice how dp[i][j] depends on dp[i-1][j-1], dp[i-1][j], and dp[i][j-1].
The final answer, the length of the LCS for the full strings, will be in the bottom-right cell, dp[n][m].
3. Reconstructing the LCS
Just like with the 0/1 Knapsack, our DP table holds the value of the optimal solution (the length), but not the solution itself (the string). To get the actual string, we must backtrack through the table.
The process is intuitive and directly follows the logic we used to build the table:
- Start at the bottom-right corner of the table:
i = n,j = m. - While
i > 0andj > 0:- If
s1.charAt(i-1) == s2.charAt(j-1):
This means the characters match and are part of the LCS. We add this character to our result and move diagonally up and to the left (i--,j--). - Else (if they don't match):
We must have arrived atdp[i][j]from the cell with the larger value. Ifdp[i-1][j] > dp[i][j-1], it means we came from above, so we move up (i--). Otherwise, we came from the left, so we move left (j--).
- If
- Since we built the string from end to beginning, we need to reverse it to get the final LCS.
Let's look at a visual of this process.

The following video provides a walkthrough of this reconstruction logic.
DP 26. Print Longest Common Subsequence | Dp on Strings
The next video in the 'take U forward' series is dedicated to printing the LCS. It clearly explains this backtracking algorithm.
Watch from 01:11 to 10:18. This section shows how to interpret the filled DP table and then use that information to trace back and build the LCS string.
Reconstruction Implementation
Now, let's look at the complete Java code that combines filling the table and then backtracking to print the result.
Print Longest Common Subsequence | (DP - 26) - Tutorial
The 'takeuforward.org' article provides a complete Java solution that first computes the DP table and then reconstructs the LCS.
Review the Java code provided. Notice how it's structured in two parts: first, the nested loops to fill the dp table, and second, the while loop to backtrack and build the lcs string.
Test your understanding!
Given the strings s1 = "ace" and s2 = "abcde", the final filled dp table for LCS length would look like this (rows correspond to s1, columns to s2):
| "" | "a" | "b" | "c" | "d" | "e" | |
|---|---|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 | 0 | 0 |
| "a" | 0 | 1 | 1 | 1 | 1 | 1 |
| "c" | 0 | 1 | 1 | 2 | 2 | 2 |
| "e" | 0 | 1 | 1 | 2 | 2 | 3 |
Starting from the bottom-right cell (value 3), trace the path backwards to reconstruct the LCS string. What is the resulting string?
Show answer
-
Start at
i=3, j=5('e'of "ace",'e'of "abcde").dp[3][5] = 3.s1.charAt(2) == 'e'ands2.charAt(4) == 'e'. They match!- Append 'e' to our result. Result: "e".
- Move diagonally to
i=2, j=4.
-
Now at
i=2, j=4('c'of "ace",'d'of "abcde").dp[2][4] = 2.s1.charAt(1) == 'c'ands2.charAt(3) == 'd'. They don't match.- Compare
dp[i-1][j](valuedp[1][4] = 1) anddp[i][j-1](valuedp[2][3] = 2). dp[2][3]is larger, so we move left toi=2, j=3.
-
Now at
i=2, j=3('c'of "ace",'c'of "abcde").dp[2][3] = 2.s1.charAt(1) == 'c'ands2.charAt(2) == 'c'. They match!- Append 'c' to our result. Result: "ec".
- Move diagonally to
i=1, j=2.
-
Now at
i=1, j=2('a'of "ace",'b'of "abcde").dp[1][2] = 1.s1.charAt(0) == 'a'ands2.charAt(1) == 'b'. They don't match.- Compare
dp[i-1][j](valuedp[0][2] = 0) anddp[i][j-1](valuedp[1][1] = 1). dp[1][1]is larger, so we move left toi=1, j=1.
-
Now at
i=1, j=1('a'of "ace",'a'of "abcde").dp[1][1] = 1.s1.charAt(0) == 'a'ands2.charAt(0) == 'a'. They match!- Append 'a' to our result. Result: "eca".
- Move diagonally to
i=0, j=0.
-
Now
i=0, so the loop terminates. -
Reverse the result "eca" to get the final LCS: "ace".
Conclusion
Well done! You have now learned how to solve one of the most fundamental dynamic programming problems. The patterns you've used today—defining a recurrence based on choices, building a 2D table, and backtracking to find the solution—are incredibly powerful and versatile.
Key Takeaways:
- The LCS problem can be solved efficiently using a 2D DP table where
dp[i][j]stores the LCS length of the firsticharacters ofs1and the firstjcharacters ofs2. - The recurrence relation depends on a simple choice: if the last characters match, we take them and solve for the smaller prefix; if not, we take the best of two smaller subproblems.
- Time Complexity: to fill the DP table.
- Space Complexity: to store the table. The reconstruction adds time but no significant extra space.
- The final LCS string is found by backtracking through the filled DP table from the bottom-right corner.
Preview of the Next Lesson:
We will continue our exploration of DP with another important subsequence problem: the Longest Increasing Subsequence (LIS). While it sounds similar, the LIS problem requires a different way of defining the DP state and opens the door to a very clever optimization that reduces the time complexity from to .