Longest Increasing Subsequence (LC 300)
On this page
Pattern: Dynamic Programming / Patience sorting + binary search
Difficulty: Medium
Key Concept: dp[i] = LIS length ending at i (O(n²)); or maintain smallest tail values per length (O(n log n)).
Problem Statement
Given an integer array nums, return the length of the longest strictly increasing subsequence.
A subsequence is derived by deleting some (or zero) elements without changing the order of the remaining elements. Strictly increasing means each chosen element is greater than the previous.
Input: int[] nums.
Output: Integer length of the LIS.
Example: [10, 9, 2, 5, 3, 7, 101, 18] → 4 (e.g. [2, 3, 7, 101]).
1. Algorithm & Pseudocode
Brute force: Enumerate all 2^n subsequences; check if strictly increasing; track max length.
DP O(n²)
dp[i] = length of LIS ending at index i
initialize dp[i] = 1 for all i
for i from 0 to n-1:
for j from 0 to i-1:
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
answer = max(dp)
O(n log n) — tails + binary search
tails = empty list (conceptually: tails[len] = smallest tail value for an increasing subsequence of that length)
for x in nums:
find position pos = first index in tails where tails[pos] >= x // lower bound
if pos == tails.size: append x
else: tails[pos] = x
return tails.size
The tails array is not the actual LIS—it stores the minimum possible tail for each length so future extensions are easiest.
2. Step-by-Step Analysis (Beginner-Friendly)
-
Why
dp[i]counts only subsequences ending ati?
When you extend a subsequence withnums[i], the previous value must be somenums[j]withj < iandnums[j] < nums[i]. Taking the bestdp[j]among suchjgives the best length ending ati. -
Why is brute force 2^n?
Each index is “in or out” of the subsequence; you check validity for each choice—exponential. -
Why does the tails method work?
For a fixed lengthL, a smaller tail is always better (more numbers can extend it). Replacing with a smallerxkeeps lengthLachievable without hurting longer lengths. -
Why binary search?
tailsis built to be sorted, so the first position wheretails[pos] >= xis found in O(log n) per element. -
Arrays.binarySearchquirk: Ifxis not found, Java returns-(insertionPoint) - 1, whereinsertionPointis wherexwould go. Sopos = -(result + 1)gives the lower-bound index.
3. The Dry Run
Sample: nums = [10, 9, 2, 5, 3, 7, 101, 18].
O(n²) DP — table of dp[i] after inner loops complete for each i:
i |
nums[i] |
dp[i] |
Reason (max over valid j) |
|---|---|---|---|
| 0 | 10 | 1 | only itself |
| 1 | 9 | 1 | no earlier smaller |
| 2 | 2 | 1 | no earlier smaller |
| 3 | 5 | 2 | extends dp[2] (2 < 5) |
| 4 | 3 | 2 | extends dp[2] (2 < 3) |
| 5 | 7 | 3 | best from dp[3] or dp[4] (5 or 3 < 7) |
| 6 | 101 | 4 | e.g. extends dp[5] |
| 7 | 18 | 4 | extends dp[5] (7 < 18) |
max(dp) = 4.
O(n log n) tails — state of tails after each x (list representation):
| Step | x |
Action | tails after |
|---|---|---|---|
| 1 | 10 | append | [10] |
| 2 | 9 | replace at 0 | [9] |
| 3 | 2 | replace at 0 | [2] |
| 4 | 5 | append | [2, 5] |
| 5 | 3 | replace at 1 | [2, 3] |
| 6 | 7 | append | [2, 3, 7] |
| 7 | 101 | append | [2, 3, 7, 101] |
| 8 | 18 | replace at 3 | [2, 3, 7, 18] |
Length = 4. Note: tails ends with [2, 3, 7, 18], not the true LIS [2, 3, 7, 101]—only the length matches.
4. Java Solution
Brute Force
Generate subsequences via bitmasking or recursion. Time: O(n × 2^n). Space: O(n).
public class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int best = 0;
int total = 1 << n;
for (int mask = 0; mask < total; mask++) {
int prev = Integer.MIN_VALUE;
int len = 0;
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) {
if (nums[i] <= prev) {
len = 0;
break;
}
prev = nums[i];
len++;
}
}
best = Math.max(best, len);
}
return best;
}
}
Optimal
O(n²) DP
public class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int best = 1;
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
best = Math.max(best, dp[i]);
}
return best;
}
}
O(n log n) — Arrays.binarySearch on tails
import java.util.Arrays;
public class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] tails = new int[n];
int size = 0;
for (int x : nums) {
int idx = Arrays.binarySearch(tails, 0, size, x);
if (idx < 0) {
idx = -(idx + 1);
}
tails[idx] = x;
if (idx == size) {
size++;
}
}
return size;
}
}
For strictly increasing, binarySearch on exact x finds an equal element if present; we need first position ≥ x. When x exists in tails[0:size), binarySearch returns some index of x, which is correct for replacement (we still replace that slot). When duplicates appear in nums, replacing the found x keeps lengths correct for strict LIS. Alternatively, implement explicit lowerBound for clarity in interviews.
5. The “Java vs. Others” Edge
Arrays.binarySearch(tails, 0, size, x): Searches only the filled prefix[0, size). Negative return →-(insertionPoint) - 1; convert with-(idx + 1)for insertion index (same idea as C++lower_boundon sorted range).- Python:
bisect_left(tails, x)is the direct analog. tailsis not the LIS: Reconstructing the actual subsequence needs parent pointers or a separate technique—many interviews only ask for length.- Strict vs non-strict: For non-decreasing LIS, use
lower_boundonx+1or adjust comparison; LC 300 is strict.
6. Complexity Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute Force | O(n × 2^n) | O(1) extra | Bitmask only practical for tiny n |
| Optimal DP | O(n²) | O(n) | dp[i] = LIS ending at i; easy to explain |
| Optimal + binary search | O(n log n) | O(n) | Patience sorting; tails sorted, binary search each x |