Longest Increasing Subsequence

Mediumdynamic programming · binary search · sorting · subsequence⏱ 6 min readUpdated

Find the longest (or count the longest) chain of elements that keeps increasing while skipping freely, with an O(n²) DP or an O(n log n) binary-search method.

PatternLongest Increasing Subsequence

Instinct Longest chain where each pick must beat the last (bigger, later, fits inside) - O(n²) best-ending-here DP, or O(n log n) tails plus binary search.

Memory hack Patience solitaire - drop each card on the leftmost pile whose top is not smaller; the number of piles is the LIS length.

At a glance

When to use it

  • You want the longest chain where each next element must be bigger, later or fit inside the previous
  • Elements keep their order but may be skipped freely
  • Items have two dimensions that can be sorted on one and chained on the other
  • n is large enough that O(n^2) is too slow

Key idea

dp[i] = 1 + the best dp[j] over earlier smaller elements gives O(n^2); keeping the smallest tail for each length and placing each element with binary search cuts it to O(n log n), versus O(2^n) for checking every subsequence.

Typical complexity

O(n log n) time, O(n) space

How to spot it If the problem says keep the order but skip freely and asks for the longest (or count of longest) strictly increasing or nesting chain, think LIS first.

The instinct

A subsequence keeps the original order but may skip elements. When the question is "longest chain where every next element is strictly greater / later / fits inside the previous one," you are looking at LIS or a dressed-up version of it.

Start with the honest DP: dp[i] = length of the longest increasing subsequence ending at index i. Every earlier j with a[j] < a[i] is a candidate predecessor, so dp[i] = 1 + max(dp[j]). That is O(n²) and often enough. When n reaches 10⁵, switch to the tails trick: keep, for every length L, the smallest possible tail value of an increasing subsequence of length L, and update it with binary search.

Recognise it

Signal in the problem Why it points here
"longest strictly increasing subsequence" The definition itself (Longest Increasing Subsequence).
"how many longest increasing subsequences" O(n²) DP carrying both length and count per index (Number of Longest Increasing Subsequence).
"envelopes / boxes / dolls fit inside each other", two dimensions Sort by one dimension, LIS on the other (Russian Doll Envelopes).
"largest subset where every pair divides" Sort, then LIS where "increasing" means "divisible by" (Largest Divisible Subset).
"minimum deletions to make the array sorted" Keep the longest increasing (or non-decreasing) part: n - LIS.
"longest chain of pairs / words where each extends the previous" Chain DP over a sorted order - LIS with a custom "can follow" test.
n up to 10⁵ with an "increasing subsequence" question O(n²) will time out; you need the binary-search version.

Not this pattern if...

  • The elements must be contiguous: a single scan or sliding window does it in O(n).
  • You compare two sequences: that is LCS (string DP).
  • You only need to know if an increasing triplet exists: a two-variable greedy suffices (Increasing Triplet Subsequence).

Memory hack

Tip

Patience solitaire: deal cards one by one. Put each card on the leftmost pile whose top card is ≥ it; if none exists, start a new pile on the right. Pile tops always stay sorted, so "leftmost valid pile" is a binary search - and the number of piles equals the LIS length.

DP framing (O(n²) version): state dp[i] = longest chain ending exactly at i; choice = which earlier j to attach to; base case dp[i] = 1 (the element alone); order = left to right, and the answer is max(dp), not dp[n-1].

Analogy: think of the tails array as a podium of record-holders: "shortest-tail champion for length 1, length 2, ...". A new number either sets a new length record (append) or lowers the bar for some existing length (replace), making future extensions easier.

Say it out loud in the interview:

  1. "dp[i] is the best chain ending at i; I look back at every smaller element - O(n²)."
  2. "To speed up, tails[L] is the smallest end of any chain of length L+1; it stays sorted, so binary search."
  3. "Tails is not the actual subsequence - only its length is meaningful; I need parent pointers to rebuild it."

How it works

For each number x, binary-search the first tail ≥ x (lower bound). If x is bigger than every tail it extends the longest chain; otherwise it replaces that tail, keeping the same length but with a smaller ending.

Trace on [10, 9, 2, 5, 3, 7, 101, 18]:

x Position found tails after Meaning
10 append [10] chain of length 1 ends at 10
9 replace 10 [9] a length-1 chain can end lower
2 replace 9 [2]
5 append [2, 5] length 2 now possible
3 replace 5 [2, 3] length 2 can end at 3
7 append [2, 3, 7]
101 append [2, 3, 7, 101] length 4
18 replace 101 [2, 3, 7, 18] same length, cheaper tail

Answer: 4 (for example 2, 3, 7, 18).

Java template

java
import java.util.*;

class LongestIncreasing {
    // O(n^2): length AND number of LIS. Change the comparison for custom "can follow" rules.
    static int[] lisLengthAndCount(int[] a) {
        int n = a.length, best = 0, ways = 0;
        int[] len = new int[n], cnt = new int[n];
        for (int i = 0; i < n; i++) {
            len[i] = 1; cnt[i] = 1;                   // base: element alone
            for (int j = 0; j < i; j++) {
                if (a[j] < a[i]) {                    // problem-specific "j can precede i"
                    if (len[j] + 1 > len[i]) { len[i] = len[j] + 1; cnt[i] = cnt[j]; }
                    else if (len[j] + 1 == len[i]) cnt[i] += cnt[j];
                }
            }
            if (len[i] > best) { best = len[i]; ways = cnt[i]; }
            else if (len[i] == best) ways += cnt[i];
        }
        return new int[]{best, ways};
    }

    // O(n log n) length only. strict=true for strictly increasing, false for non-decreasing.
    static int lisLength(int[] a, boolean strict) {
        int[] tails = new int[a.length];
        int size = 0;
        for (int x : a) {
            int lo = 0, hi = size;                    // search in tails[0..size)
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                boolean goRight = strict ? tails[mid] < x : tails[mid] <= x;
                if (goRight) lo = mid + 1; else hi = mid;
            }
            tails[lo] = x;                            // replace or append
            if (lo == size) size++;
        }
        return size;
    }

    // 2-D nesting: sort width ascending, height DESCENDING on ties, then LIS on heights.
    static int maxNesting(int[][] env) {
        Arrays.sort(env, (p, q) -> p[0] != q[0] ? Integer.compare(p[0], q[0]) : Integer.compare(q[1], p[1]));
        int[] h = new int[env.length];
        for (int i = 0; i < env.length; i++) h[i] = env[i][1];
        return lisLength(h, true);
    }
}

Complexity

Approach Time Space Notes
DP over predecessors O(n²) O(n) Supports counting and custom "can follow" rules
Tails + binary search O(n log n) O(n) Length only, unless you add parent indices
Sort + LIS (envelopes) O(n log n) O(n) Sorting dominates along with the search

Variations

  • Plain length: Longest Increasing Subsequence - either approach; mention both and their trade-off.
  • Counting: Number of Longest Increasing Subsequence - keep len[] and cnt[]; on a tie in length, add counts instead of replacing.
  • 2-D nesting: Russian Doll Envelopes - sort by width ascending and height descending for equal widths, then strict LIS on heights.
  • Divisibility chain: Largest Divisible Subset - sort, use a[i] % a[j] == 0 as the predecessor test, keep parent pointers to output the set.
  • Word chains: Longest String Chain - sort by length; predecessor is any word formed by deleting one character (hash map lookup).
  • Non-decreasing variant: allow equal neighbours by using upper bound (first tail > x) instead of lower bound.
  • Bitonic / mountain: Minimum Number of Removals to Make Mountain Array - LIS from the left plus LIS from the right at each peak.

Common mistakes

Warning

The tails array is not necessarily a real subsequence. For [3, 4, 5, 1] it ends as [1, 4, 5], yet 1 comes after 5 in the input. Use it for the length only; reconstruct with parent pointers if the actual sequence is required.

  • Returning dp[n-1]. The longest chain need not end at the last element; return the maximum over all i.
  • Lower vs upper bound mix-up. Strictly increasing uses "first ≥ x"; non-decreasing uses "first > x". Getting it wrong miscounts duplicates.
  • Ascending tie-break in Russian Doll Envelopes. Equal widths sorted by ascending height let two same-width envelopes nest; sort heights descending on ties.
  • Counting LIS with the tails method. Counting needs the O(n²) DP (or a Fenwick tree); tails loses the information.
  • Forgetting the base case dp[i] = 1, which leaves single elements at length 0.

All practice problems

3 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.

My notes saved in this browser