HARD Lab · Dynamic Programming · 30

300. Longest Increasing Subsequence

Find the length of the longest strictly increasing subsequence in an array — not necessarily contiguous. The O(n²) DP builds it element by element; the O(n log n) patience-sorttrick uses binary search on a "tails" array to solve it in near-linear time.

MediumDPPatience SortingTypeScript

PROBLEM What we're solving

Given nums = [10,9,2,5,3,7,101,18], return the length of the longest strictly increasing subsequence (elements need not be contiguous). Here the answer is 4: the subsequence [2,3,7,101] (or [2,5,7,101], etc.). Note that order must be preserved — we pick indices left-to-right.

KEY IDEA Each element knows the longest chain ending at it

Insight → Define dp[i] = the length of the longest increasing subsequence that ends exactly at index i. Then dp[i] = 1 + max(dp[j]) over all j < i with nums[j] < nums[i]. Every element starts as a chain of length 1, then greedily extends the best valid predecessor. The answer is max(dp).

RECURRENCE O(n²) DP recipe

  • 0 · Initialize. Set dp[i] = 1 for every i — each element alone is a valid LIS of length 1.
  • 1 · Outer loop i = 1 … n−1. We want the best chain ending at i.
  • 2 · Inner loop j = 0 … i−1. If nums[j] < nums[i], then j can precede i; try dp[i] = max(dp[i], dp[j]+1).
  • 3 · Track global best. After the inner loop, best = max(best, dp[i]).
  • 4 · Return best. O(n²) time, O(n) space.
Classic confusion → Forgetting that dp[i] represents the LIS ending at i — not the LIS of the prefix nums[0..i]. The answer is max(dp), not dp[n-1]. The last element is rarely in the longest chain.

COST Complexity & the O(n log n) upgrade

O(n²) DP
O(n²)
Two nested loops; O(n) space.
Patience sort + binary search
O(n log n)
One pass with binary search; O(n) space.

Patience sorting idea

Maintain a tails array where tails[k] is the smallest tail element of all increasing subsequences of length k+1 seen so far. For each new x, binary-search for the first tails entry ≥ x. If found, replace it (keeping the tail as small as possible for future extensions). If not found, append — extending the longest chain. The length of tails at the end is the LIS length. tails is always sorted, so the binary search is valid.

Pattern transfer → The patience-sort binary-search pattern also solves Russian Doll Envelopes (2D LIS), Number of LIS (count variants), and feeds into Longest Common Subsequence reduction. The O(n²) DP framework is reused in Longest Bitonic Subsequence and Maximum Sum Increasing Subsequence.

RUN IT O(n²) DP — dp[i] = 1 + max(dp[j]) for valid predecessors

step 0 / 45
STARTInput: [10, 9, 2, 5, 3, 7, 101, 18]. Initialize dp[i] = 1 for all i — every element is an LIS of length 1 by itself.
10091225334751016187
State
i
j
1
dp[i]
1
best
current ij being comparedj extends dp[i]
slowfast

TYPESCRIPT The solution, annotated

lengthOfLIS.ts
function lengthOfLIS(nums: number[]): number {
  const n = nums.length;
  if (n === 0) return 0;

  // dp[i] = length of LIS ending at index i
  const dp: number[] = new Array(n).fill(1);

  let best = 1;
  for (let i = 1; i < n; i++) {
    for (let 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) patience-sorting approach ──────────────────────────────────────
// Maintain a "tails" array: tails[k] is the smallest tail of all increasing
// subsequences of length k+1. Binary search to place each element.
function lengthOfLISFast(nums: number[]): number {
  const tails: number[] = [];           // tails[k] = min tail of LIS length k+1

  for (const x of nums) {
    let lo = 0, hi = tails.length;
    while (lo < hi) {                   // binary search for first tail >= x
      const mid = (lo + hi) >> 1;
      if (tails[mid] < x) lo = mid + 1;
      else hi = mid;
    }
    tails[lo] = x;                      // extend or replace
  }
  return tails.length;                  // number of "piles" = LIS length
}

Reading it block by block

Lines 4–7 — initialize dp. Every index begins as a chain of length 1; it can always extend itself. best tracks the global maximum as we fill the table.
Lines 9–14 — O(n²) double loop. For each i, scan every j < i. When nums[j] < nums[i], j is a valid predecessor; take the best: dp[i] = max(dp[i], dp[j]+1). After the inner loop, fold dp[i] into best.
Lines 21–35 — patience-sort O(n log n). tails[k] stores the smallest possible tail of any increasing subsequence of length k+1. Because tails stays sorted, we binary-search for the insertion point of x (first entry ≥ x). Replacing keeps tails as small as possible — greedy choice that maximizes future extension potential. Appending grows the longest known chain by one. The array's length at the end is the LIS length.
Correctness of tails. tails itself is notthe actual LIS — it's a maintenance structure. What we proved is: (1) tails is always strictly increasing (invariant), so binary search works; (2) its length equals the length of the longest IS seen. To recover the actual subsequence you need a separate parent[] array (see follow-up below).
Complexity → O(n²) time and O(n) space for the DP solution. O(n log n) time and O(n) space for patience sort — the binary search over a sorted tails array costs O(log n) per element, O(n) total elements.

INTERVIEWFollow-ups they'll ask

  • "Return the actual subsequence, not just its length." Add a parent[] array: when dp[i] is updated from j, set parent[i] = j. Trace back from the index where dp[idx] === best.
  • "How many distinct LIS exist?" Add a count[] parallel to dp[]: count[i] += count[j] whenever dp[j]+1 === dp[i]. Sum count[i] where dp[i] === best.
  • "Non-strictly increasing (allow equal)?" Change the inner condition from nums[j] < nums[i] to nums[j] ≤ nums[i]. In patience sort, binary-search for the first entry > x instead of ≥ x.
  • "2D version — Russian Doll Envelopes?" Sort by width ascending, then by height descending for equal widths. Now run 1D LIS on the heights only. The descending-height trick prevents picking two envelopes of the same width.
  • "What's the brute-force complexity?" Enumerating all 2ⁿ subsequences to find the longest increasing one is O(2ⁿ). The O(n²) DP is an exponential improvement; O(n log n) is the further refinement.

MNEMONIC The one-liner

"Every element anchors a chain — extend the best predecessor, or start fresh at 1."

TRIGGERS When you see ___ → reach for ___

"longest/count subsequence (not subarray)"DP with dp[i] = LIS ending at i
"strictly increasing order preserved"nums[j] < nums[i] predecessor check
"O(n log n) LIS or smallest tail greedy"patience sort with binary search on tails[]
"2D LIS, nested envelopes, chain of pairs"sort + reduce to 1D LIS (patience sort)

SKELETON The reusable shape

skeleton.ts
// O(n²) DP
const dp = new Array(nums.length).fill(1);
let best = 1;
for (let i = 1; i < nums.length; i++) {
  for (let 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) patience sort
const tails: number[] = [];
for (const x of nums) {
  let lo = 0, hi = tails.length;
  while (lo < hi) { const mid = (lo+hi)>>1; if (tails[mid]<x) lo=mid+1; else hi=mid; }
  tails[lo] = x;
}
return tails.length;

FLASHCARDS Tap to flip

What does dp[i] represent in the LIS DP?
The length of the longest increasing subsequence that ends exactly at index i. Not the LIS of the prefix.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
For nums = [10,9,2,5,3,7,101,18], what is the LIS length?
QUESTION 02
What does dp[i] represent in the O(n²) DP?
QUESTION 03
What is the time complexity of the O(n²) DP and the patience-sort solution?
QUESTION 04
In patience sort, when you find an existing tails entry ≥ x, you:
QUESTION 05
nums = [3,1,2]. What is dp[] after running the O(n²) DP?
QUESTION 06
Why does patience sort keep tails[] sorted at all times?
QUESTION 07
How do you adapt the solution to return the actual subsequence (not just its length)?