1. State Inversion: From $O(N^2)$ Prefix DP to $O(N \log N)$ Monotonic Tails
The textbook $O(N^2)$ recurrence defines $dp[i]$ as the length of the Longest Increasing Subsequence ending at $A[i]$: $dp[i] = 1 + \max_{j < i, A[j] < A[i]} dp[j]$. To break the $O(N^2)$ barrier, we apply State-Value Inversion:
Define $\text{tails}[L]$ (0-indexed for length $L + 1$) as the minimum possible ending value among all strictly increasing subsequences of length $L + 1$ seen so far in prefix $A[0..i]$.
Core Invariant — Strict Monotonicity of the Patience Sorting tails Array
After processing any prefix $A[0..i]$, the array $\text{tails}$ satisfies:
$$\text{tails}[0] < \text{tails}[1] < \dots < \text{tails}[k - 1]$$Proof: If $\text{tails}[L] \le \text{tails}[L-1]$ were ever true, then taking the second-to-last element of the length-$(L+1)$ subsequence ending in $\text{tails}[L]$ would give a valid increasing subsequence of length $L$ with ending value $< \text{tails}[L] \le \text{tails}[L-1]$, contradicting the minimality of $\text{tails}[L-1]$!
Because $\text{tails}$ is sorted, inserting $x = A[i]$ replaces the first element $\ge x$ via std::lower_bound (for strictly increasing $<$) or std::upper_bound (for non-decreasing $\le$) in $O(\log N)$ time.
2. Patience Sorting & Dilworth's Theorem (Chain / Antichain Duality)
Imagine dealing cards $A[0..n-1]$ into piles from left to right according to Patience Sorting (Aldous & Diaconis, 1999): place card $x$ on the leftmost pile whose top card is $\ge x$ (or start a new pile to the right if all pile tops are $< x$). Each pile is a non-increasing subsequence, and the top cards of the piles from left to right are literally our array $\text{tails}[0..k-1]$!
- Dilworth's Theorem (1950): In any finite partially ordered set, the minimum number of disjoint chains (non-increasing subsequences) needed to partition the sequence equals the maximum size of an antichain (strictly increasing subsequence). Thus:
- Minimum number of non-increasing subsequences to cover $A$ $=$ Length of Strictly Increasing LIS (
lower_bound). - Minimum number of strictly decreasing subsequences to cover $A$ $=$ Length of Non-Decreasing LNDS (
upper_bound).
- Minimum number of non-increasing subsequences to cover $A$ $=$ Length of Strictly Increasing LIS (
- 2D Envelope / Russian Doll Nesting $(w_i, h_i)$: To find the longest chain where both $w_a < w_b$ and $h_a < h_b$, sort pairs by $w_i$ ascending, and on ties by $h_i$ DESCENDING, then run 1D strictly increasing LIS on $h_i$! Sorting ties by $h_i$ descending guarantees we can never pick two envelopes with the same width $w$.
4 -> 4 -> 3
Top = 3"] --> P2["Pile 1 (L=2):
10 -> 8
Top = 8"] P2 --> P3["Pile 2 (L=3):
9
Top = 9"] end P3 --> Result["LIS Length = 3 (Number of Piles)
3 Non-Increasing Chains Partition A:
[4, 4, 3], [10, 8], [9]"]
Hardware Reality — Why tails Alone Is NOT the LIS (Reconstruction via Index Tracking)
A notorious interview and production bug is returning the final tails vector itself as the LIS! On $A = [10, 20, 30, 5]$, after processing $30$ we have tails = [10, 20, 30], and when $5$ arrives it overwrites tails[0] = 5, leaving tails = [5, 20, 30]—which is not a valid subsequence because $5$ appeared after $20$ and $30$! To reconstruct the true sequence in $O(N \log N)$ time, store the array index tail_idx[pos] = i and record parent[i] = (pos > 0 ? tail_idx[pos - 1] : -1) at the moment $A[i]$ is placed at position pos.
3. Complexity Matrix
Observe why 0/1 Knapsack iterates capacity w downwards (preventing item reuse), how Patience Sorting finds LIS in O(N log N), and how Myers Diff slides along diagonal snakes k = x - y.
Invariant & State
| LIS Variant / Algorithm | Binary Search Comparator | Time Complexity | Auxiliary Space | Output Capability |
|---|---|---|---|---|
| Naive Prefix DP ($dp[i]$) | Linear Scan $j < i$ | O(N^2) | O(N) | Length + Count + Sequence |
| Patience Sorting (Strictly Increasing $<$) | std::lower_bound ($ge x$) |
O(N log N) | O(N) | Length + Reconstructed LIS |
| Patience Sorting (Non-Decreasing $le$) | std::upper_bound ($> x$) |
O(N log N) | O(N) | Length + Reconstructed LNDS |
| Count of All LIS / Weighted LIS | Fenwick / Segment Tree on ranks | O(N log N) | O(N) | Max Weighted LIS + Exact Count |
4. Reference C++20 Implementation
#include <algorithm>
#include <cstdint>
#include <vector>
// 1. O(N log N) Strictly Increasing Subsequence with Full Sequence Reconstruction
std::vector<int> longest_increasing_subsequence(const std::vector<int>& nums) {
const int n = static_cast<int>(nums.size());
if (n == 0) return {};
std::vector<int> tails; // tails[len] = smallest ending value of LIS of length len+1
std::vector<int> tail_idx; // tail_idx[len] = index in nums that achieved tails[len]
std::vector<int> parent(n, -1); // parent[i] = predecessor index in nums for element i
for (int i = 0; i < n; ++i) {
auto it = std::lower_bound(tails.begin(), tails.end(), nums[i]);
int pos = static_cast<int>(it - tails.begin());
if (it == tails.end()) {
tails.push_back(nums[i]);
tail_idx.push_back(i);
} else {
*it = nums[i];
tail_idx[pos] = i;
}
if (pos > 0) {
parent[i] = tail_idx[pos - 1];
}
}
// Reconstruct valid subsequence backward from the index of the longest pile top
std::vector<int> result(tails.size());
int curr = tail_idx.back();
for (int k = static_cast<int>(tails.size()) - 1; k >= 0; --k) {
result[k] = nums[curr];
curr = parent[curr];
}
return result;
}
// 2. 2D Russian Doll Envelopes: Sort (w ASC, h DESC) + 1D LIS on h
int max_nested_envelopes(std::vector<std::pair<int, int>> envelopes) {
std::sort(envelopes.begin(), envelopes.end(), [](const auto& a, const auto& b) {
if (a.first != b.first) return a.first < b.first;
return a.second > b.second; // DESCENDING height on equal width prevents duplicate width picks!
});
std::vector<int> tails;
for (const auto& [w, h] : envelopes) {
auto it = std::lower_bound(tails.begin(), tails.end(), h);
if (it == tails.end()) tails.push_back(h);
else *it = h;
}
return static_cast<int>(tails.size());
}
Production Pitfall — Using lower_bound Instead of upper_bound for Non-Decreasing Subsequences
When a problem asks for the Longest Non-Decreasing Subsequence ($A[i_1] \le A[i_2] \le \dots$), equal elements are allowed to extend an existing chain! If you use std::lower_bound ($ge x$), an equal element overwrites the existing tail instead of extending it. Always remember the pairing: Strictly Increasing ($<$) $\iff$ lower_bound; Non-Decreasing ($le$) $\iff$ upper_bound.