Dynamic Programming · Classic DP Patterns

Longest Increasing Subsequence

Computing, for each index, the best subsequence ending exactly there by checking every earlier index that could legally precede it (a smaller value) and extending whichever earlier answer is best - the same O(n^2) DP shape answers both "longest length" and "maximum sum" just by changing what gets tracked and combined.

  • 5 min read
  • 8 practice questions
  • 3 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: O(n log n) with tails
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

The task: given a sequence, find the longest subsequence, not necessarily contiguous, whose values strictly increase. Variants ask for the heaviest such subsequence, or the longest chain of nested boxes. Trying all 2^n subsequences is hopeless. The DP insight is to ask a narrower question at each position: what's the best subsequence that ends exactly here? That gives an O(n²) DP, and a "tails" array brings the length down to O(n log n).

We'll go from simple to more technical:

  1. The pattern: dp[i] = the longest increasing subsequence ending at i.
  2. A second flavor: O(n log n) with the tails array and binary search.
  3. Where LIS shows up (version diffs, nesting, scheduling).
  4. What breaks it: reading the answer from the wrong place, tails mistaken for the answer, strict vs non-strict.

The approach

Java

int lengthOfLIS(int[] nums) {
    int n = nums.length, best = 1;
    int[] dp = new int[n];                     // dp[i] = longest increasing subsequence ENDING at i
    for (int i = 0; i < n; i++) {
        dp[i] = 1;                             // nums[i] on its own
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);   // extend a chain ending at j
        }
        best = Math.max(best, dp[i]);          // the answer can end anywhere
    }
    return best;
}

Python

def length_of_lis(nums: list[int]) -> int:
    n, best = len(nums), 1
    dp = [1] * n                                # dp[i] = longest increasing subsequence ENDING at i
    for i in range(n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)   # extend a chain ending at j
        best = max(best, dp[i])                 # the answer can end anywhere
    return best

Fixing the last element makes the recurrence easy: a chain ending at i is some chain ending at an earlier, smaller value, plus nums[i]. The answer is the maximum over every i - on [1, 2, 0], dp at the last index is 1, but the answer is 2. Changing the combine step gives relatives: dp[j] + nums[i] with base nums[i] finds the maximum-sum increasing subsequence.

A second flavor: O(n log n) with tails

For the length alone, keep tails[k] = the smallest possible last value of an increasing subsequence of length k + 1. A smaller tail is never worse - it leaves more room to extend. Each value replaces the first tail that is >= it, or extends the list:

Java

int[] tails = new int[n];
int len = 0;
for (int x : nums) {
    int lo = 0, hi = len;
    while (lo < hi) { int mid = (lo + hi) >>> 1; if (tails[mid] < x) lo = mid + 1; else hi = mid; }
    tails[lo] = x;                             // x becomes the best tail for length lo + 1
    if (lo == len) len++;
}
return len;

Python

from bisect import bisect_left


def length_of_lis(nums: list[int]) -> int:
    tails = []
    for x in nums:
        i = bisect_left(tails, x)               # the first tail >= x
        if i == len(tails):
            tails.append(x)                     # x extends the longest chain
        else:
            tails[i] = x                        # x becomes the best tail for length i + 1
    return len(tails)

tails stays sorted, so each step is a binary search: O(n log n) - about 1.7 million steps for n = 10^5, against 5 billion for the quadratic loop. Two cautions: tails is not an actual subsequence (for [3, 4, 1] it ends as [1, 4]), and "strictly increasing" needs the first tail >= x; for non-decreasing, search for the first tail > x ([2, 2, 2] then has length 3, not 1). In Python, bisect.bisect_left(tails, x) is exactly "the first tail >= x", and bisect_right the first tail > x. Nested envelopes reduce to this: sort by width ascending and, for equal widths, height descending, then run LIS on heights - the descending tie-break stops two envelopes of the same width from both joining a chain.

Where you'll see it

Longest-increasing-subsequence logic shows up in version-control and patience diff algorithms (matching unique lines in order), in scheduling chains of compatible tasks, in stacking and nesting problems (boxes, envelopes), and in measuring how "sorted" a sequence already is - n minus the LIS is the minimum number of elements to move to make it sorted.

Which tool for which job?

You needUse
LIS length, small n or custom rulesdp[i] over all j < i, O(n²)
LIS length, large ntails plus binary search, O(n log n)
The subsequence itselftails plus parent indices
The heaviest increasing subsequencea max-Fenwick tree over values, O(n log n)
Nested envelopessort by width up and height down on ties, then LIS on heights
LCS of two permutationsLIS of positions, O(n log n)

Common mistakes

MistakeWhat happensDo this instead
Returning dp[n - 1]misses LIS ending earlierthe max over all dp[i]
Reading tails as the subsequence[3, 4, 1] gives [1, 4], which isn't a subsequencekeep parent indices
The wrong search for strict vs non-strict[2, 2, 2] gives 3 or 1 wronglyfirst tail ≥ x (strict), first > x (non-strict)
Arrays.binarySearch with duplicatesreturns any match, not the firstwrite the lower-bound search
Envelopes sorted with heights ascending on tiesequal widths chain togetherheights descending on ties

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why is the answer max(dp[i]) and not dp[n - 1]?

Show answer

dp[i] is the best subsequence ending exactly at i. The longest one can end anywhere - for [1, 2, 0], the chain 1, 2 ends at index 1 while dp[2] is only 1.

2. After running the tails method on [3, 4, 1], tails is [1, 4]. Is that an increasing subsequence of the input?

Show answer

No - 1 comes after 4 in the input. tails[k] is the smallest tail of some length-(k + 1) subsequence; different entries can come from different subsequences. Only the length (2) is guaranteed. Keep parent indices to rebuild an actual subsequence.

3. For nested envelopes, why sort equal widths by height descending before running LIS on heights?

Show answer

Envelopes of the same width can't nest, but LIS on heights would happily chain [1, 1], [1, 2], [1, 3] if they appeared in increasing height order. Putting equal widths in decreasing height order guarantees at most one of them can join any strictly increasing chain.

Key takeaways

  • dp[i] = best subsequence ending exactly at i; extend from every compatible earlier j; the answer is the max over all i.
  • O(n^2) and flexible: swap the combine step for sums, counts or custom compatibility.
  • For the length: tails + binary search, O(n log n); tails is not the subsequence itself.
  • Strictly increasing uses the first tail >= x; non-decreasing uses the first tail > x.
  • 2D chains (envelopes) reduce to LIS after sorting by one dimension, with a descending tie-break.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Length, small nO(n²) dp
Length, large ntails, O(n log n)
The sequencetails plus parents
Max summax-Fenwick over values
Envelopeswidth up, height down, then LIS
LCS of permutationsLIS of positions

Snippets worth knowing by heart

Java

int lo = 0, hi = len;                                 // first tail >= x (strict LIS)
while (lo < hi) { int mid = (lo + hi) >>> 1; if (tails[mid] < x) lo = mid + 1; else hi = mid; }
tails[lo] = x;
if (lo == len) len++;

Python

k = bisect.bisect_left(tails, x)          # bisect_right for non-decreasing
if k == len(tails):
    tails.append(x); tail_idx.append(i)
else:
    tails[k] = x; tail_idx[k] = i
parent[i] = tail_idx[k - 1] if k else -1

The traps

  • dp[n - 1] as the answer: the LIS may end earlier. Take the max over all dp[i].
  • tails as the subsequence: [3, 4, 1] leaves [1, 4], which isn't one. Keep parent indices to rebuild it.
  • Strict vs non-strict search: [2, 2, 2] gives the wrong length. Find the first tail >= x for strict, > x for non-strict.
  • Arrays.binarySearch with duplicates returns any match, not the first. Write the lower-bound search.
  • Envelope tie order: equal widths with heights ascending chain together. Sort heights descending on ties.

Staff-level points, one line each

  • Tails plus parents rebuilt a real maximum-length LIS on 500 arrays.
  • Max-sum increasing subsequence with a max-Fenwick tree matched O(n²) on 500 arrays.
  • LCS of two permutations equaled the LIS of positions on 100 pairs.
  • A random permutation of 100,000 had an LIS of 620; 2√n ≈ 632.
  • Java tails on 1,000,000 ints took under 2 seconds. binarySearch([2,2,2,2,2], 2) returned 2, not 0.

Say it in an interview

  • "dp[i] is the best ending at i, and the answer is the max over all i."
  • "Tails keeps the smallest end for each length, so binary search gives O(n log n)."
  • "To get the sequence back I keep parent indices."
  • "LCS of two permutations becomes an LIS of positions."

Practice

This skill comes with 8 multiple-choice questions that tell you which misconception a wrong answer reveals and 3 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Longest Increasing Subsequence free

Longest Increasing Subsequence

MediumProCoding problemJava · Python

Find the longest subsequence of nums (elements kept in their original order, gaps allowed) in which every number is bigger than the one before it, and return its length.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Maximum Sum Increasing Subsequence

MediumProCoding problemJava · Python

Given an integer array nums of positive integers, find the maximum sum achievable by a strictly increasing subsequence.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Russian Doll Envelopes

HardProCoding problemJava · Python

Each envelope has a width and a height, and one fits inside another only when it is strictly narrower and strictly shorter. Return the most envelopes you can nest, one inside the next.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Longest Increasing Subsequence free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime