DP — Longest Increasing Subsequence Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide
dp[i] = length of the best increasing chain that ends at i. Faster version: keep the smallest possible tail for each length.
3 sub-patterns · 12 questions
What is this pattern?
LIS: pick elements in order (not necessarily next to each other) so each one is bigger than the one before, and make the chain as long as possible.
- O(N²):
dp[i]= the length of the longest increasing chain that ends ati. It is1 + max dp[j]over every earlierjwitha[j] < a[i](or 1 if there is none). The answer is the biggestdp[i]. - O(N log N): keep
tails[k]= the smallest possible last value of an increasing chain of lengthk + 1.tailsis sorted, so each new number finds its place with binary search (lower_bound): it either extends the longest chain or replaces a bigger tail.
The same "ends at i, built from an earlier j" idea works whenever you can say which earlier item may come before this one: divides it, is a word with one letter removed, is smaller in both dimensions. For 2-D items, sort by one dimension first, then run LIS on the other.
Stacking cards in piles (patience sorting): each card goes on the leftmost pile whose top card is not smaller than it, or starts a new pile. The number of piles at the end is the length of the longest increasing subsequence — and the pile tops are exactly the tails array.
See it move
Purple = earlier values smaller than nums[i] (they can come before it), red = not smaller, green = the one chosen.
nums = [3, 1, 4, 1, 5, 9, 2, 6]step 1/10dp[i] = the length of the longest increasing chain that ends exactly at nums[i]. parent[i] = the index before i in that chain (− = none).
pos = lower_bound(x) in tails. Green = appended (a longer chain), orange = replaced (same length, smaller tail).
nums = [3, 1, 4, 1, 5, 9, 2, 6]step 1/10tails[k] = the smallest last value of any increasing chain of length k + 1 seen so far. It stays sorted, so we can binary search it.
How do I recognise it?
"Longest increasing subsequence" — order matters, gaps are allowed.
A chain where each item must fit after the previous one: divisible by it, one letter longer, bigger in both width and height.
Pairs / boxes / envelopes / people that must be sorted before they can be chained.
"How many longest increasing subsequences", "maximum sum increasing subsequence" — the same table with a different value per cell.
"Return the chain itself" → keep a parent index per cell and walk back.
Looks like DP — Longest Increasing Subsequence, but isn't
Contiguous means each element only extends the run right before it. LIS lets you skip elements, so any earlier element can be the one before.
LCS compares two sequences with a 2-D table. LIS has one sequence and a rule about order between its own elements.
It looks like a chain, but picking the interval that ends first is always safe, so no DP is needed.
No chain is being built; you just sweep the sorted intervals.
How would I discover it from scratch?
Don't memorise “use dp — longest increasing subsequence”. Ask these questions in order — the pattern falls out.
Try every subsequence: take or skip each element, remembering the last one taken. O(2^N).
Sub-patterns
Same core idea, different rule for “valid”. Learn them in this order.
Classic LIS
4 questionsThe plain LIS and its cousins: length, count of longest chains, maximum-sum chain, or just "is there a chain of 3".
Chains and reconstruction
4 questionsSame dp[i] = 1 + max dp[j], with a different "may come before" rule, and often a parent array to return the chain itself.
2D / envelopes
4 questionsItems with two (or three) dimensions. Sort by one dimension so the order is fixed, then LIS on the other.
Don't mix these up
Can you skip elements? Yes → LIS with dp over every earlier j. No → one pass that resets on a drop.
Strictly increasing (no equal neighbours) → lower_bound. Non-decreasing (equal values allowed) → upper_bound.
tails.size() is the length. The values in tails can come from different chains; rebuild with parents if the chain is needed.
Strict in both → sort ties descending, lower_bound. Equal allowed on the second → sort ties ascending, upper_bound.
Code templates (C++)
LIS: O(N²) table and O(N log N) tails
lower_bound = strictly increasing; upper_bound = non-decreasing (equal values may repeat).
// O(N^2): dp[i] = length of the longest increasing chain that ends at i.
int lisQuadratic(const vector<int>& a) {
int n = a.size(), best = 0;
vector<int> dp(n, 1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++)
if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);
best = max(best, dp[i]);
}
return best;
}
// O(N log N): tails[k] = smallest last value of an increasing chain of length k + 1.
// tails is sorted; it gives the LENGTH only, not the chain itself.
int lisFast(const vector<int>& a) {
vector<int> tails;
for (int x : a) {
auto it = lower_bound(tails.begin(), tails.end(), x); // upper_bound for non-decreasing
if (it == tails.end()) tails.push_back(x); // longer than every chain so far
else *it = x; // a smaller tail for that length
}
return tails.size();
}Chain with a custom rule + rebuild
Only canFollow(j, i) changes between problems (divides, one letter added, …). parent lets you walk back from the best end.
// Longest chain with a custom "j may come before i" rule, returned as the actual chain.
// Shown for divisibility (Largest Divisible Subset); sort first so the rule only looks back.
vector<int> longestChain(vector<int> a) {
sort(a.begin(), a.end());
int n = a.size(), end = 0;
vector<int> dp(n, 1), parent(n, -1);
auto canFollow = [&](int j, int i) { return a[i] % a[j] == 0; };
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++)
if (canFollow(j, i) && dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; parent[i] = j; }
if (dp[i] > dp[end]) end = i;
}
vector<int> chain;
for (int i = n ? end : -1; i != -1; i = parent[i]) chain.push_back(a[i]); // walk back
reverse(chain.begin(), chain.end());
return chain;
}2-D: sort, then LIS
The sort order is the whole trick: ascending by width, descending by height for equal widths, then strict LIS on heights.
// 2-D chain, strictly bigger in both dimensions (Russian Doll Envelopes).
// Sort width ascending, and height DESCENDING for equal widths, then strict LIS on heights:
// two envelopes of the same width can never both be in one increasing run.
int maxNested(vector<vector<int>> env) {
sort(env.begin(), env.end(), [](const vector<int>& x, const vector<int>& y) {
return x[0] != y[0] ? x[0] < y[0] : x[1] > y[1];
});
vector<int> tails;
for (auto& e : env) {
auto it = lower_bound(tails.begin(), tails.end(), e[1]);
if (it == tails.end()) tails.push_back(e[1]);
else *it = e[1];
}
return tails.size();
}