Hello! Welcome to our next lesson on Dynamic Programming.
In our previous session, we tackled the Longest Common Subsequence (LCS) problem. We saw how a 2D DP table could systematically find the optimal solution for two strings, and how to backtrack through it to reconstruct the subsequence. This O(n*m) approach is a cornerstone of DP on strings.
Today, we'll shift our focus to a problem that works on a single sequence but introduces new and powerful DP concepts: the Longest Increasing Subsequence (LIS). Your learning outcome is to solve the LIS problem using both an O(n²) DP approach and an optimized O(n log n) approach. Mastering LIS is crucial as it's a frequent guest in technical interviews and introduces an optimization pattern that is very valuable for competitive programming.
1. The Longest Increasing Subsequence Problem
A subsequence, as we recall, is formed by deleting zero or more elements from a sequence without changing the order of the remaining elements. An increasing subsequence is a subsequence where the elements are in strictly increasing order. The goal of the LIS problem is to find the length of the longest such subsequence in a given array.
For example, in the array [10, 9, 2, 5, 3, 7, 101, 18]:
[2, 3, 7, 18]is an increasing subsequence.[2, 3, 7, 101]is another, longer, increasing subsequence.- The LIS is
[2, 3, 7, 101](or[2, 5, 7, 101], etc.), and its length is 4.
2. The Standard DP Approach: O(n²)
Just like with other DP problems, the key is to define a subproblem. For LIS, a very effective way to define the state is:
Let dp[i] be the length of the Longest Increasing Subsequence that ends at index i.
With this definition, how can we compute dp[i]? The LIS ending at index i will be formed by taking the element nums[i] and appending it to some LIS that ends at a previous index j (where j < i), provided that nums[j] < nums[i]. To get the longest possible sequence ending at i, we should look for the maximum length among all valid previous subsequences.
This leads to the recurrence relation:dp[i] = 1 + max(dp[j]) for all 0 <= j < i where nums[j] < nums[i].
The + 1 accounts for the element nums[i] itself. If no such j exists (i.e., nums[i] is smaller than all preceding elements), then the LIS ending at i is just the element itself, so dp[i] = 1. This serves as our base case.
The following video provides an excellent visualization of this process. It frames the problem as finding the longest path in a Directed Acyclic Graph (DAG), which is a powerful way to think about many DP problems.
5 Simple Steps for Solving Dynamic Programming Problems
This video from the Reducible channel breaks down the LIS problem into five clear steps, guiding you from problem visualization to the final O(n²) implementation.
Watch from 00:48 to 07:57. Focus on these key points: The definition of the subproblem: LIS ending at a specific index k. How the relationship between subproblems is established. The final implementation strategy where you iterate and solve subproblems from left to right.
Implementation and Final Answer
After computing the dp array for all indices, the job is not quite done. Remember that dp[i] is the length of the LIS ending at i. The overall LIS of the entire array could end at any index. Therefore, the final answer is the maximum value in the dp array.
Let's look at a concise Java implementation of this logic.
Longest Increasing Subsequence in Java
This article from Medium provides a clean Java implementation for the O(n²) dynamic programming method. Pay attention to how the code translates the recurrence relation we just discussed.
Read the section titled 'Dynamic Programming Method'. Examine the findLIS function. Notice the two nested loops, which lead to the O(n²) complexity. See how the inner loop calculates dp[i] by checking all previous dp[j] values, and how a final loop is needed to find the overall maximum.
The time complexity is O(n²) due to the nested loops, and the space complexity is O(n) for the dp array. This is a solid solution and a great starting point.
3. The Optimized Approach: O(n log n)
An O(n²) solution is often too slow for competitive programming or interviews involving large datasets (e.g., n = 10⁵). We can do better. The bottleneck in our previous approach was the inner loop: for each element, we linearly scanned all previous elements. We can optimize this search.
The core idea is to maintain an auxiliary array (let's call it tails) that stores the smallest possible tail element for an increasing subsequence of a given length.
Specifically, tails[i] will hold the smallest tail element of all increasing subsequences that have a length of i+1.
By its definition, this tails array will always be sorted. This is the key property that allows for optimization.
Here’s the algorithm:
- Initialize an empty list
tails. - Iterate through each number
numin the input arraynums. - For each
num, use binary search ontailsto find the first element that is greater than or equal tonum.- Case 1:
numis greater than all elements intails. This meansnumcan extend the longest subsequence found so far. We appendnumtotails. The length of the LIS increases by 1. - Case 2:
numis not greater than all elements. The binary search finds an indexiwheretails[i]is the smallest element greater than or equal tonum. We replacetails[i]withnum. This does not change the LIS length, but it gives us a new increasing subsequence of lengthi+1with a smaller tail. A smaller tail increases our chances of finding longer subsequences later.
- Case 1:
After iterating through all numbers, the final length of the LIS is simply the size of the tails list.
Important: The tails list itself is not the LIS. It is a clever auxiliary structure used to efficiently track the length.

Let's watch a video that explains this "patient sorting" analogy and walks through the Java code. The explanation of how to use Collections.binarySearch is particularly helpful.
LeetCode 300. Longest Increasing Subsequence - O(n log n)
This video from Kacy Codes provides an excellent walkthrough of the O(n log n) solution. It starts with the 'patient sorting' analogy and then dives into a clear Java implementation.
Watch from the beginning until 09:01. Focus on: The concept of 'piles' (our tails array). The rule for placing a new number: either start a new pile or place it on top of the first pile it's smaller than or equal to. How the number of piles gives the LIS length. The critical part: how Collections.binarySearch in Java returns a negative value for the insertion point and how to decode it to get the correct index.
Implementation Details
As the video explained, Collections.binarySearch in Java has a specific behavior when an element is not found. It returns -(insertion point) - 1. Your task is to transform this negative value back into the correct index where the element should be inserted to maintain order.
index = -(index + 1)
This optimized approach brings the time complexity down to O(n log n) because for each of the n elements, we perform a binary search which takes O(log n) time. The space complexity remains O(n) for the tails list.
Test your understanding!
Let's trace the O(n log n) algorithm for the input array [4, 10, 6, 12]. You start with an empty tails list. What is the state of the tails list after processing each number?
num = 4:num = 10:num = 6:num = 12:
What is the final size of the tails list?
Show answer
num = 4:tailsis empty. 4 is added.tailsis now[4].num = 10: 10 is greater than all elements intails. 10 is added.tailsis now[4, 10].num = 6: Binary search for 6 in[4, 10]. The smallest element>= 6is 10 at index 1. We replace 10 with 6.tailsis now[4, 6]. (Note: The LIS length is still 2, but we now have a better sequence[4, 6]to build upon).num = 12: 12 is greater than all elements intails. 12 is added.tailsis now[4, 6, 12].
The final tails list is [4, 6, 12]. Its size is 3, which is the length of the LIS.
Conclusion
Excellent work! You've now added another significant DP problem to your toolkit and, more importantly, learned a powerful optimization technique. The jump from O(n²) to O(n log n) by replacing a linear scan with a binary search is a pattern that appears in several other problems.
Key Takeaways:
- O(n²) Approach:
- DP State:
dp[i]= length of LIS ending at indexi. - Recurrence:
dp[i] = 1 + max(dp[j])for validj < i. - The final answer is the max value in the
dparray.
- DP State:
- O(n log n) Approach:
- Maintains a sorted
tailslist wheretails[i]is the smallest end element of an increasing subsequence of lengthi+1. - Uses binary search to find the correct position for each number in
O(log n). - The final answer is the size of the
tailslist.
- Maintains a sorted
- Understanding the trade-offs: The
O(n²)solution is often more intuitive to code initially, but theO(n log n)version is far more efficient and necessary for performance-critical scenarios.
Preview of the Next Lesson:
We will continue with dynamic programming by exploring the Matrix Chain Multiplication problem. This problem has a different flavor from the subsequence problems we've seen. It involves finding the most efficient way to parenthesize a sequence of matrix multiplications, and it will introduce you to a common DP pattern used for interval or bracketing problems.