Hello! Welcome back to our series on classic dynamic programming applications.
In the last lesson, we explored the Longest Common Subsequence (LCS) problem, where we compared two sequences to find the longest subsequence they share. You learned how to formulate the 2D DP solution, fill the table, and backtrack to reconstruct the result.
Today, we'll tackle a related but distinct problem: solving the Longest Increasing Subsequence (LIS) problem using dynamic programming. This problem involves only a single sequence and requires a clever subproblem definition that is a cornerstone of DP thinking. Mastering this will further solidify your understanding of how to identify and leverage optimal substructure.
1. Defining the Longest Increasing Subsequence
First, let's be clear about what we're looking for. A subsequence retains the relative order of elements but doesn't require them to be contiguous. An increasing subsequence is a subsequence where each element is strictly greater than the one before it. The LIS is simply the longest one you can find.
For example, given the sequence A = [10, 9, 2, 5, 3, 7, 101, 18]:
[2, 5, 7, 101]is an increasing subsequence of length 4.[2, 3, 7, 18]is another increasing subsequence of length 4.[9, 101]is an increasing subsequence of length 2.- The LIS has a length of 4.
Longest Increasing Subsequence (LeetCode 300) | Detailed solution with animations and diagrams
To get a solid grasp of the problem, including some interesting edge cases, watch the first few minutes of this video from Nikhil Lohia.
Watch from the beginning at 01:22 until 05:16. The video provides clear examples and visual aids to define the LIS problem.
2. The Challenge of Defining the Subproblem
With LCS, our subproblem dp[i][j] naturally depended on two sequences. Here, we only have one. A naive first attempt might be to define LIS(i) as the length of the LIS in the suffix A[i:].
However, this approach quickly runs into a wall. If we compute LIS(i+1), we know the length of the longest increasing subsequence in the rest of the array, but we don't know what value it starts or ends with. Without that information, we can't decide if A[i] can be prepended to it. This lack of information breaks the optimal substructure property.
16. Dynamic Programming, Part 2: LCS, LIS, Coins
Professor Erik Demaine from MIT OpenCourseWare walks through this exact thought process, explaining precisely why the naive subproblem definition is insufficient. This is key to understanding the 'why' behind the correct formulation.
Watch from 23:51 to 31:07. Pay close attention to how he identifies the flaw in the initial approach and introduces the crucial constraint that makes the subproblem solvable.
3. The Correct DP Formulation
As the video highlights, the solution is to add a constraint to our subproblem. The standard and most intuitive way to do this for a bottom-up implementation is to define dp[i] as the length of the longest increasing subsequence that ends at index i.
With this definition, to compute dp[i], we can look at all previous indices j < i. If A[i] > A[j], it means we can potentially extend the LIS that ended at j. We would form a new LIS of length dp[j] + 1 ending at i. Since we want the longest such subsequence, we look at all valid j's and take the one that gives the maximum length.
This gives us the recurrence relation:
If no such j exists (i.e., A[i] is smaller than all preceding elements), the LIS ending at i is just the element A[i] itself, so its length is 1. We can consider the max over an empty set to be 0, simplifying the formula.
Important Note: The final answer is not dp[n-1]. Since the LIS can end at any index, the answer is the maximum value in the entire dp array: max(dp[0], dp[1], ..., dp[n-1]).
Visualizing LIS as a Graph Problem
Another powerful way to conceptualize this problem, which your background in engineering and software might make appealing, is to view it as finding the longest path in a Directed Acyclic Graph (DAG).
- Each number in the sequence is a node.
- A directed edge exists from node
A[i]toA[j]ifi < jandA[i] < A[j].
The LIS problem is now equivalent to finding the longest path in this DAG. The DP approach we're using is, in fact, a way to compute this longest path.

4. Tabulation Walkthrough and Implementation
Let's see this in action. We'll create a dp array of the same size as our input array and initialize all its values to 1 (since any single element is an LIS of length 1). Then, we iterate through the array, applying our recurrence.

Longest Increasing Subsequence (LeetCode 300) | Detailed solution with animations and diagrams
The Nikhil Lohia video provides a fantastic, animated walkthrough of this tabulation process. It clearly shows how the dp array is filled step-by-step.
Watch the detailed dry run from 09:07 to 19:16. This will make the abstract recurrence relation very concrete.
For a more formal and textual representation of the algorithm, the following resource provides clear pseudocode.
Dynamic Programming 1: Longest increasing subsequence
This PDF from a course at the University of Colorado provides a concise, academic summary of the algorithm and its correctness.
Read 'Algorithm 1' and the accompanying 'Correctness' proof. This formalizes the logic you just saw in the video.
5. Complexity Analysis and Reconstructing the Sequence
Complexity
- Time Complexity: . We have a nested loop structure. The outer loop runs
ntimes (forifrom 0 ton-1), and the inner loop runsitimes (forjfrom 0 toi-1). This gives us a quadratic number of comparisons. - Space Complexity: . We need an auxiliary array
dpof sizento store the lengths of the subproblems.
Reconstructing the LIS
Similar to the LCS and Knapsack problems, our dp table contains enough information to find the length, but to get the actual sequence, we need to store the "path" we took. We can do this by maintaining a predecessor array. When we update dp[i] using dp[j], we set predecessor[i] = j.
Once the dp array is filled, we find the index max_idx where the maximum dp value occurs. This is the last element of our LIS. We can then backtrack from max_idx using our predecessor array to reconstruct the full sequence.
Longest Increasing Subsequence (LeetCode 300) | Detailed solution with animations and diagrams
This process is also covered very clearly in the Nikhil Lohia video.
Watch from 21:38 to 26:38 to see how to use a second array to keep track of the path and reconstruct the final LIS.
Conclusion
Excellent work today. You've now solved the Longest Increasing Subsequence problem, a classic DP challenge that requires a more nuanced subproblem definition than what we've seen before. Understanding how to define dp[i] as the LIS ending at that position is a key takeaway.
Key Takeaways:
- The LIS problem requires a subproblem definition with a constraint:
dp[i]is the length of the LIS ending at indexi. - The recurrence relation is
dp[i] = 1 + max({dp[j]})for allj < iwhereA[j] < A[i]. - The final answer is the maximum value in the
dparray, not necessarily the last element. - The algorithm has an time complexity and space complexity.
- The actual subsequence can be reconstructed by storing predecessors and backtracking from the index of the maximum
dpvalue.
Preview of the Next Lesson:
We will continue our journey through sequence-based DP by looking at the Edit Distance (or Levenshtein Distance) problem. This will feel like a return to the structure of LCS, as it involves comparing two strings, but we'll be measuring their difference by counting insertions, deletions, and substitutions. It's a fundamental algorithm in natural language processing and computational biology.