Welcome back! In our previous lesson, we established a strong foundation in 1D Dynamic Programming, mastering the memoization and tabulation techniques. You learned to identify the recurrence relation in a problem and optimize its implementation, a crucial skill for tackling complex algorithmic challenges.
Today, we'll extend those concepts into a new dimension by tackling 2D Dynamic Programming problems. These problems involve states defined by two (or more) parameters, often leading to solutions that use a 2D table. Our focus will be the Longest Common Subsequence (LCS) problem, a cornerstone of DP that appears frequently in technical interviews and has direct applications in real-world systems like version control. By the end of this lesson, you'll be able to apply the same systematic DP thinking to solve this more complex class of problems.
1. Defining the Problem: Longest Common Subsequence (LCS)
First, it's critical to understand what a "subsequence" is and how it differs from a "substring." This distinction is a common interview trap. A substring must be contiguous, while a subsequence only needs to maintain the relative order of characters, allowing for gaps.
For example, given two strings s1 = "ABCDE" and s2 = "ACE":
- The longest common substring is just
"A". - The longest common subsequence is
"ACE", with a length of 3. The characters A, C, and E appear in both strings in the same order, but not necessarily next to each other.
The diff tool used in version control systems like Git is a practical application of this concept, finding the differences between two files by identifying their longest common subsequence.
For a concise explanation of this crucial difference, the following article is an excellent starting point.
Longest Common Subsequence: A Visual Walkthrough | Codeintuition
This section from Codeintuition clearly defines the LCS problem and emphasizes the critical distinction between a subsequence and a substring.
Read the section What the longest common subsequence is. Pay close attention to the example and the explanation of why this distinction is fundamental to setting up the correct DP solution.
2. Crafting the Recurrence Relation
Just as with 1D DP, the heart of the solution lies in a recurrence relation that breaks the problem into smaller, overlapping subproblems. Let's define lcs(i, j) as the length of the longest common subsequence between the prefixes s1[0...i] and s2[0...j].
To find lcs(i, j), we only need to look at the last characters of these prefixes, s1[i] and s2[j]. This leads to two simple cases:
- Characters Match: If
s1[i] == s2[j], then this character is part of the LCS. We can add 1 to our count and find the LCS of the remaining shorter prefixes:1 + lcs(i-1, j-1). - Characters Don't Match: If
s1[i] != s2[j], we can't use both characters. The LCS must be the longest of the two possibilities: either we discards1[i]and findlcs(i-1, j), or we discards2[j]and findlcs(i, j-1). We take the maximum of these two outcomes:max(lcs(i-1, j), lcs(i, j-1)).
The base case is when one of the strings is empty (i.e., i < 0 or j < 0), in which case the LCS length is 0.
A naive recursive implementation of this logic would be incredibly slow due to re-computing the same subproblems repeatedly, as illustrated in the recursion tree below.

The following video provides an excellent, detailed walkthrough of this entire thought process, from the initial recursive idea to the final optimized solution. We'll start with the sections that develop this recurrence.
Dp 25. Longest Common Subsequence | Top Down | Bottom-Up | Space Optimised | DP on Strings
This video from the "take U forward" channel meticulously breaks down the LCS problem, starting with the definition and building the recurrence relation from first principles.
Problem Definition: First, watch the introduction from start off to solidify your understanding of subsequences and the problem statement. Developing the Recurrence: The core logic is developed from we'll try to write some recurrence. This segment explains the two cases (match vs. not match) and how they translate into recursive calls. Base Case: The base case is explained from what is the other thing that's remaining. Recursion Tree: Finally, watch the recursion tree walkthrough from more clarity to see the overlapping subproblems in action.
3. Implementation Strategies: Memoization and Tabulation
Now that we have the recurrence, we can implement it using the two methods we learned in the last lesson.
Memoization (Top-Down)
This is the most direct translation of our recursive logic. We write the recursive function as defined above and add a 2D array, memo[m][n], to cache the results of lcs(i, j). Before computing, we check the cache; after computing, we store the result.
The AlgoMaster.io article provides a clear, step-by-step guide from the brute-force recursion to the memoized solution.
Longest Common Subsequence | DSA | AlgoMaster.io
This article shows the logical progression from a naive recursive approach to an efficient top-down DP solution.
First, quickly review the Brute Force section to see the inefficient recursive algorithm. Then, read the Top-Down DP (Memoization) section carefully. It explains exactly how to add a memoization table to fix the performance issue, reducing the time complexity from exponential to O(m \times n).
Tabulation (Bottom-Up)
The tabulation approach builds the solution iteratively, eliminating recursion. We create a 2D table dp[m+1][n+1], where dp[i][j] stores the LCS length for the first i characters of s1 and the first j characters of s2.
Using an (m+1) x (n+1) table allows us to use the 0-th row and column to represent the base cases (empty strings), which simplifies the logic. We then fill the table using our recurrence:
- If
s1[i-1] == s2[j-1]:dp[i][j] = 1 + dp[i-1][j-1](take from diagonal) - Else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])(take max of top and left)
The final answer is in dp[m][n].
The NeetCode video offers a great visual explanation of how this table-filling process works.
Longest Common Subsequence - Dynamic Programming - Leetcode 1143
This video by NeetCode provides a highly intuitive, visual walkthrough of the bottom-up DP approach.
Watch from take a look. He does an excellent job of showing how the 2D grid is initialized and how each cell is filled based on the two core rules (match vs. no match), effectively building the solution from the "bottom up."
This DP table illustrates the final state for finding the LCS of strings "ACADB" and "CBDA". The arrows show the dependencies: a diagonal arrow for a character match, and horizontal/vertical arrows for mismatches. The final answer, 2, is in the bottom-right cell.

4. Advanced Topics: Space Optimization and Reconstruction
In an interview, simply arriving at the time and space solution is good, but discussing further optimizations demonstrates senior-level thinking.
Space Optimization
Looking at the tabulation recurrence, notice that to compute any cell dp[i][j], we only need values from the current row (dp[i]) and the previous row (dp[i-1]). We never need to look at rows i-2 or earlier. This means we can optimize the space complexity from to by only storing two rows at a time.
The AlgoMaster.io article has a dedicated section explaining this powerful technique.
Longest Common Subsequence | DSA | AlgoMaster.io
This section explains how to reduce the memory footprint of the tabulation solution.
Read the section on Space-Optimized DP. The key idea is to use two 1D arrays (prev and curr) to represent the previous and current rows of the DP table, drastically reducing space usage.
Reconstructing the LCS
The DP table gives us the length of the LCS. A common follow-up question is to produce the actual subsequence string. This requires backtracking through the completed dp table from the bottom-right corner (dp[m][n]).
- If
s1[i-1] == s2[j-1], this character is part of the LCS. Add it to your result and move diagonally up-left todp[i-1][j-1]. - Otherwise, move to the larger of the two neighbors: up to
dp[i-1][j]or left todp[i][j-1]. - Repeat until you reach the first row or column.
Crucially, this reconstruction process requires the full DP table. You cannot reconstruct the path using the space-optimized version. Mentioning this trade-off—that you sacrifice the ability to reconstruct the sequence to save space—is a key insight to share with an interviewer.
The Codeintuition article explains this process and its trade-offs perfectly.
Longest Common Subsequence: A Visual Walkthrough | Codeintuition
This resource explains how to backtrack through the DP table to find the actual subsequence and highlights important trade-offs.
Read the section Recovering the actual subsequence to understand the backtracking algorithm. Then, read the "Common mistakes and how to avoid them" section, paying special attention to the warning about space optimization. This is a vital point for interviews.
Conclusion
In this lesson, we successfully transitioned from 1D to 2D Dynamic Programming by tackling the Longest Common Subsequence problem. You've seen how to derive the recurrence relation, implement it with both memoization and tabulation, and apply critical optimizations.
Key Takeaways:
- 2D DP State: Problems like LCS require a state defined by two parameters (e.g., indices
iandjinto two strings), leading naturally to a 2D DP table. - LCS Recurrence: The solution is built on two simple cases: if characters match, take
1 + diagonal; if not, takemax(top, left). - Implementation Progression: The path from naive recursion -> memoization -> tabulation -> space optimization is a powerful framework for solving many DP problems.
- Space vs. Reconstruction: The space-optimized solution can only find the length of the LCS. Reconstructing the subsequence itself requires the full table. Knowing and communicating this trade-off is essential.
You now have the tools to solve a significant category of interview problems. However, solving the problem is only half the battle. In our next lesson, we will focus on the other half: how to effectively structure and communicate your solution during an interview using frameworks like UMPIRE. This will help you present your strong technical skills in a clear, confident, and impressive manner.