LeetCode #300 Medium

Longest Increasing Subsequence

Length of the longest strictly increasing subsequence (elements keep order, need not be adjacent).

dpbinary-search
Open on LeetCode ↗
02

Intuition

Classic DP: dp[i] = length of the best increasing subsequence ending at i, built by scanning all smaller earlier elements. The O(n log n) upgrade keeps a list tails where tails[k] is the smallest possible tail of an increasing subsequence of length k+1 — each new number either extends the list or improves (lowers) one tail via binary search.

How to spot this pattern

The O(n log n) version is worth recognising as a patience sorting problem: tails[i] is the smallest possible tail of any increasing subsequence of length i + 1. Keeping tails as small as possible leaves the most room to extend later, and because that array is sorted by construction you can binary search it. Whenever a DP array turns out to be monotone, a binary search can usually replace the inner loop.

03

Approach

1

O(n²) DP first

dp[i] = 1 + max(dp[j]) over all j < i with nums[j] < nums[i]. Answer is max(dp). Simple, and the base for many follow-up problems.

2

The tails insight

Among all increasing subsequences of length L, only the one with the smallest tail matters — it is the easiest to extend. So keep just that smallest tail per length: tails is sorted, enabling binary search.

3

Patience step

For each num: find the first tail ≥ num (bisect_left). If none, num extends the longest run (append). Otherwise it replaces that tail — same length, better (smaller) tail. Length of tails is the answer.

04

Solution & live demo

1from bisect import bisect_left
2 
3class Solution:
4 def lengthOfLIS(self, nums):
5 tails = []
6 for x in nums:
7 i = bisect_left(tails, x)
8 if i == len(tails):
9 tails.append(x) # extends longest subsequence
10 else:
11 tails[i] = x # same length, smaller tail
12 return len(tails)
05

Common pitfalls

Reading tails as the actual subsequence

✗ Wrong
return tails   # the LIS itself
✓ Right
return len(tails)

Only the length is meaningful. On [10, 9, 2, 5, 3, 7] the array ends as [2, 3, 7], which is a genuine LIS here, but on other inputs it holds tails from different subsequences that never coexisted. Reconstructing the real sequence needs separate predecessor tracking.

Using bisect_right instead of bisect_left

✗ Wrong
i = bisect_right(tails, x)
✓ Right
i = bisect_left(tails, x)

bisect_right places a duplicate after its equals, extending the run and counting a non-strict increase. For a strictly increasing subsequence, an equal value must replace its match, which is what bisect_left does. (Flip it deliberately when the problem asks for non-decreasing.)

Appending whenever the value is larger

✗ Wrong
if x > tails[-1]:
    tails.append(x)
else:
    tails[i] = x
✓ Right
if i == len(tails):
    tails.append(x)
else:
    tails[i] = x

The two tests agree, but the i == len(tails) form derives the decision from the search you already paid for, and it doesn't crash on the first element when tails is still empty.

06

Edge cases

Strictly decreasing input

Every number replaces tails[0]; answer 1.

Duplicates

bisect_left makes equal values replace, not extend — enforcing strict increase.

tails is not the LIS itself

Only its length is meaningful; elements may come from different subsequences.

07

Complexity

Time
O(n log n)
Space
O(n)
One binary search per element; O(n²) DP variant also accepted.