Longest increasing subsequence
The longest run of values that only go up, picked in order from an array. O(n²) with a simple DP, O(n log n) with binary search.
Keep tails[k], the smallest value that can end an increasing subsequence of length k + 1. Each new value appends to tails or replaces the first tail >= it.
You want the longest chain that increases, the fewest deletions to make an array sorted, or nesting boxes and envelopes after a sort.
O(n log n), or O(n²) for the plain DP
O(n)
You’ll recognise it when
- You pick elements in their original order (skipping any you like) and the picks must keep going up.
- The question asks for the fewest deletions that leave an array sorted: that’s
nminus the LIS. - Items nest or stack only if they’re bigger in two dimensions (envelopes, boxes, people on shoulders). Sort by one dimension and it becomes an LIS on the other.
nis up to 10⁵, so the O(n²) DP is too slow and you need the O(n log n) version.
It’s easy to confuse with the longest increasing subarray, where the elements must be next to each other; that one is a single linear scan.
The idea
Deal a shuffled deck of numbered cards into piles, left to right. Each card goes on the leftmost pile whose top card is at least as big; if there is none, it starts a new pile on the right. The number of piles at the end is the length of the longest increasing run you could have picked from the deck. A small top card is good news: it lets more future cards land on the pile after it.
Start with the plain DP. Let dp[i] be the length of the longest increasing subsequence that ends at a[i]. It is 1 (just a[i]) plus the best dp[j] over earlier j with a smaller value. The answer is the largest dp[i], not the last one:
def lis_quadratic(a):
dp = [1] * len(a) # dp[i]: longest increasing run ending at a[i]
for i in range(len(a)):
for j in range(i):
if a[j] < a[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp, default=0)
That’s O(n²): fine for a few thousand values, too slow for 10⁵. The fast version keeps less. For each length it remembers only the smallest value that can end a run of that length, in an array called tails. Among all runs of length 3, the one ending lowest is the easiest to extend, so it’s the only one worth keeping. tails is always sorted, which means binary search can find where each new value belongs.
How it works
Read the values left to right and update tails for each one. The graphic follows the steps as you scroll. Turn on quiz me to predict each slot yourself, or edit the array: try duplicates, or a decreasing run.
- Start with an empty
tails.tails[k]will be the smallest value that can end an increasing subsequence of lengthk + 1. - Take the next value
x. Every step asks one question: which run canxfinish best? - If
xis bigger than every tail, it extends the longest run: append it. The LIS so far just got one longer. Thelenrow under the input recordspos + 1for each value: the longest run ending there, the same number asdp[i]in the slow DP. - Otherwise
xreplaces a tail. Here 2 replaces 5: a run of length 1 ending in 2 is better than one ending in 5, because more values can follow it. - To find the slot, binary search for the first tail
>= x, the lower bound. Forx = 6in[2, 8], that’spos = 1: 2 is smaller, 8 is not. tails[pos] = x. Nothing gets longer, but the length-2 run now ends at 6 instead of 8. Every tail only ever goes down, andtailsstays sorted.- A value equal to a tail lands on that tail and changes nothing. That’s what makes this the strictly increasing version: a second 3 can’t follow the first.
- Watch
tailshere:[1, 3, 6, 7]. The 1 appears after the 7 in the input, so this is not a real subsequence. Each slot is a different run’s best ending; only the length is guaranteed. - When every value is placed, the LIS length is
len(tails). - To get an actual LIS, remember for each value the tail it extended (the value at
pos - 1when it was placed). Start at the last tail and follow those parent links back. - The chain gives 2, 3, 6, 7, a real increasing subsequence of length 4.
tailshad the right length but the wrong values.
Why it’s correct: after each step, tails[k] is the smallest ending value of any increasing run of length k + 1 seen so far. A new x can extend every run whose end is below x; the longest such run has length pos, the number of tails smaller than x. So x ends a run of length pos + 1, and since tails[pos] >= x, x is the new smallest ending for that length. No other slot changes. tails only gets longer when a longer run exists, so its final length is the LIS.
def lis_length(a):
"""Length of the longest strictly increasing subsequence of a."""
tails = []
for x in a:
# Lower bound: the first slot whose tail is >= x.
lo, hi = 0, len(tails)
while lo < hi:
mid = (lo + hi) // 2
if tails[mid] < x:
lo = mid + 1
else:
hi = mid
pos = lo
if pos == len(tails):
tails.append(x)
else:
tails[pos] = x
return len(tails)#include <vector>
using namespace std;
// Length of the longest strictly increasing subsequence of a.
int lisLength(const vector<int>& a) {
vector<int> tails;
for (int x : a) {
// Lower bound: the first slot whose tail is >= x.
int lo = 0, hi = tails.size();
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (tails[mid] < x) lo = mid + 1;
else hi = mid;
}
int pos = lo;
if (pos == (int)tails.size()) {
tails.push_back(x);
} else {
tails[pos] = x;
}
}
return tails.size();
}class Lis {
// Length of the longest strictly increasing subsequence of a.
static int lisLength(int[] a) {
int[] tails = new int[a.length];
int len = 0;
for (int x : a) {
// Lower bound: the first slot whose tail is >= x.
int lo = 0, hi = len;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (tails[mid] < x) lo = mid + 1;
else hi = mid;
}
int pos = lo;
if (pos == len) {
tails[len++] = x;
} else {
tails[pos] = x;
}
}
return len;
}
}The code keeps values only, which is all the length needs. To rebuild a sequence, keep indices in tails plus a parent array, as the graphic does:
from bisect import bisect_left
def lis_sequence(a):
tails, tail_idx = [], [] # tail values, and where each came from
parent = [-1] * len(a)
for i, x in enumerate(a):
pos = bisect_left(tails, x)
if pos > 0:
parent[i] = tail_idx[pos - 1]
if pos == len(tails):
tails.append(x); tail_idx.append(i)
else:
tails[pos] = x; tail_idx[pos] = i
seq, i = [], tail_idx[-1] if a else -1
while i != -1:
seq.append(a[i]); i = parent[i]
return seq[::-1]
Why it’s O(n log n)
Each of the n values does one binary search over tails, which has at most n entries: O(log n) per value, O(n log n) total. tails and the parent links take O(n) space.
| Version | Time | Space | n = 10⁵ |
|---|---|---|---|
dp[i] over all j < i |
O(n²) | O(n) | 5 × 10⁹ steps, too slow |
tails + binary search |
O(n log n) | O(n) | about 1.7 × 10⁶ steps |
The O(n²) DP is still worth knowing: it adapts easily when the rule isn’t just “smaller value”, for example counting runs or adding weights.
Common mistakes
Returning dp[-1] in the O(n²) version
dp[i] is the best run ending at i, and the best run needn’t end at the last element. On [1, 2, 3, 0], dp[-1] is 1 but the answer is 3.
return dp[-1] # ✗ best run ending at the last value
return max(dp, default=0) # ✓ best run ending anywhere
Printing tails as the answer
tails has the right length, but it mixes endings from different runs. On [3, 1, 4, 1, 5, 9, 2, 6] it ends as [1, 2, 5, 6], yet the 2 comes after the 5 in the input. Keep parent links if you need the sequence.
Upper bound when you want strictly increasing
With bisect_right, an x equal to a tail goes after it, so equal values extend the run. That computes the longest non-decreasing subsequence. On [2, 2, 2] it returns 3 instead of 1.
pos = bisect_right(tails, x) # ✗ counts 2, 2, 2 as increasing
pos = bisect_left(tails, x) # ✓ equal values replace, never extend
Envelopes sorted with both keys ascending
In Russian doll envelopes, sorting by width then height, both ascending, lets two envelopes of the same width both join the height LIS, even though they can’t nest. Sort heights descending within the same width.
env.sort() # ✗ (1,1), (1,2), (1,3) → 3
env.sort(key=lambda e: (e[0], -e[1])) # ✓ heights 3, 2, 1 → 1
Variations
- Reconstructing the sequence. Store indices in
tailsand aparentfor every element, then walk back from the last tail, as in the snippet above. With the O(n²) DP, record whichjgave the bestdp[i]. - Non-decreasing subsequence. Allow equal neighbours by using the upper bound (
bisect_right,std::upper_bound) instead of the lower bound. CSES Towers is this in disguise: the fewest towers equals the longest non-decreasing run. - Russian doll envelopes. Sort by width ascending and, for equal widths, height descending. Then the answer is the strict LIS of the heights.
- Number of LISs. Next to
dp[i], keepcount[i], the number of best runs ending ati: whendp[j] + 1beatsdp[i], copycount[j]; when it ties, add it. Sum the counts of everyiwith the maximumdp[i]. This is O(n²). - Longest decreasing, and deletions. For a decreasing run, negate the values or reverse the comparison. The fewest deletions to leave an array strictly increasing is
nminus its LIS.
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
Envelope
(w, h)fits inside another only if both its width and height are strictly smaller. With up to 10^5 envelopes, how do you find the longest chain of nested envelopes?Once widths are sorted, a chain is an increasing run of heights. Sorting heights descending inside one width means two envelopes of equal width can never both be picked. With both ascending, equal widths like (1,1), (1,2) would wrongly nest. The graph works but has up to n² edges, and area order says nothing about each side.
-
2
What does this print?
import bisecttails = []for x in [3, 1, 4, 1, 5, 9, 2, 6]:i = bisect.bisect_left(tails, x)if i == len(tails):tails.append(x)else:tails[i] = xprint(tails, len(tails))tails[k]is the smallest value that can end a run of length k + 1, so its length, 4, is the LIS length. But the list mixes endings from different runs: the 2 comes after the 5 in the input, so[1, 2, 5, 6]isn’t a subsequence.[1, 4, 5, 9]is a real LIS, and recovering one needs parent links. -
3
This O(n²) LIS has a bug. What does it print?
def lis(a):dp = [1] * len(a)for i in range(len(a)):for j in range(i):if a[j] < a[i]:dp[i] = max(dp[i], dp[j] + 1)return dp[-1]print(lis([1, 2, 3, 0]))dp[i]is the longest run ending at index i. The best run, 1, 2, 3, ends at index 2, but the function returnsdp[-1], and nothing before the 0 is smaller than it, so it prints 1. The fix isreturn max(dp), which gives 3. -
4
What does this print?
import bisectdef lis_len(a, search):tails = []for x in a:i = search(tails, x)if i == len(tails):tails.append(x)else:tails[i] = xreturn len(tails)a = [2, 2, 2, 1, 2]print(lis_len(a, bisect.bisect_left), lis_len(a, bisect.bisect_right))bisect_leftmakes an equal value replace its tail, so duplicates never extend a run: that’s the strictly increasing LIS, 1, 2, of length 2.bisect_rightplaces an equal value after its twin, which computes the longest non-decreasing run: 2, 2, 2, 2, length 4. It’s not 5, because the 1 can’t sit between 2s. -
5
The
dp[i]= “longest run ending at i” DP and thetailsmethod both find the LIS. What are their running times?The DP compares every
iwith every earlierj, about n²/2 pairs. Thetailsmethod does one binary search per value on a sorted array of at most n entries. It isn’t O(n): each value still needs a search, and there’s no general linear-time LIS algorithm for comparisons.
Practice problems
- easy Longest Continuous Increasing Subsequence leetcode.com
- medium Longest Increasing Subsequence leetcode.com
- medium Increasing Subsequence cses.fi
- medium Towers cses.fi
- medium Number of Longest Increasing Subsequence leetcode.com
- hard Russian Doll Envelopes leetcode.com
- hard Minimum Number of Removals to Make Mountain Array leetcode.com