Medium#3008 min readDynamic Programming1D DPBinary SearchLeetCode

Longest Increasing Subsequence

Patience sort with binary search

O(n²) DP first, then learn the O(n log n) patience sorting approach.

Published Apr 16, 2026

Problem

Given an integer array nums, return the length of the longest strictly increasing subsequence. A subsequence keeps order but may drop elements; it does not have to be contiguous.

Input
nums = [10, 9, 2, 5, 3, 7, 101, 18]
Output
4
Explanation
One valid LIS is [2, 3, 7, 18] (others exist).
Input
nums = [0, 1, 0, 3, 2, 3]
Output
4
Explanation
LIS: [0, 1, 2, 3].
Input
nums = [7, 7, 7, 7, 7]
Output
1
Explanation
Strictly increasing means equal values can't chain.

Intuition

What does dp[i] depend on? The longest increasing subsequence ending at i is one more than the longest ending at some earlier j where nums[j] < nums[i]:

dp[i] = 1 + max(dp[j] for j < i if nums[j] < nums[i])

That's quadratic. We scan every earlier position for each i.

The n log n trick is a reframe: we don't actually need dp per index — we only need the smallest possible tail of an increasing subsequence of each length. Keep a sorted array tails where tails[k] = the smallest value that any length-(k + 1) increasing subsequence can end in. For each new x:

  • If x is larger than every tail, it extends the longest sequence by one — push it.
  • Otherwise, it replaces the first tail >= x. The subsequence of that length now ends on a smaller value, which is strictly better for extending later.

Final length of tails is the LIS length. The replacement uses binary search, giving O(n log n).

ℹ️
Note

tails is not an LIS itself — the actual subsequence may interleave values that got overwritten. You only get the length for free. Reconstructing the LIS needs back-pointers (see #673 Number of LIS).

Approach

Quadratic DP

TimeO(n²)
SpaceO(n)

The direct translation of the recurrence. Easy to derive, easy to debug, plenty fast for n 2500. If the interview constraint allows it, this is often the right answer — clarity beats cleverness.

impl Solution {
    pub fn length_of_lis(nums: Vec<i32>) -> i32 {
        let n = nums.len();
        if n == 0 {
            return 0;
        }
        let mut dp = vec![1i32; n];
        let mut best = 1;
        for i in 1..n {
            for j in 0..i {
                if nums[j] < nums[i] {
                    dp[i]= dp[i].max(dp[j] + 1);
                }
            }
            best= best.max(dp[i]);
        }
        best
    }
}
Rust · runnable

Patience Sort + Binary Search

TimeO(n log n)
SpaceO(n)
Recommended

Maintain tails, a sorted array of smallest tails per length. For each x, binary-search the first tails[k] >= x (strict increase → >=, not >). If none exists, append; otherwise overwrite.

The binary search is the entire speedup. Rust's Vec::partition_point returns the index where a predicate flips from true to false — exactly what we need for "first tail >= x". In TypeScript we write the binary search by hand.

impl Solution {
    pub fn length_of_lis(nums: Vec<i32>) -> i32 {
        let mut tails: Vec<i32> = Vec::with_capacity(nums.len());
        for x in nums {
            // First index where tails[k] >= x.
            let k = tails.partition_point(|&t| t < x);
            if k= tails.len() {
                tails.push(x);
            } else {
                tails[k]= x;
            }
        }
        tails.len() as i32
    }
}
Rust · runnable

Trace

Patience sort on nums = [10, 9, 2, 5, 3, 7, 101, 18]:

ixactiontails
010push[10]
19replace 10[9]
22replace 9[2]
35push[2, 5]
43replace 5[2, 3]
57push[2, 3, 7]
6101push[2, 3, 7, 101]
718replace 101[2, 3, 7, 18]

Final tails.len() = 4. ✓ Note the final tails content looks like a plausible LIS, but that's coincidence — if the replacements crossed each other earlier, the stored values would no longer form a real subsequence of the input.

Edge cases

  • nums = [] — return 0. The partition_point approach handles this naturally (empty tails); the DP form needs the explicit check.
  • Strictly increasing input — every element pushes; tails.len() equals nums.len().
  • All equal — return 1. Because the comparison is < (strict), every x after the first replaces tails[0] without extending.
  • Duplicates in a generally-increasing sequence — replace semantics keep the smallest tail at each length, so [1, 2, 2, 3] returns 3, not 4.
💡
Tip

Swap < for <= in the binary-search predicate to solve the non-strict variant (longest non-decreasing subsequence). Same algorithm; one character of difference.

Takeaway

When a DP recurrence looks like "max over all earlier states matching a predicate," check if the state space has extra structure you can exploit. Here the structure is: for each length, only the smallest tail matters. That's what lets us replace a per-index DP with a single sorted array.

This patience-sort reframe is one of the oldest tricks in competitive programming and shows up whenever "maintain best-so-far per category" is enough to answer the outer question. The same shape reappears in #673 (count LIS with back-pointers), #354 Russian Doll Envelopes (2D LIS after sort), and in the offline-query optimizations that underpin monotonic structures like #1143 LCS.

More in 1D DP