Longest Increasing Subsequence
Key Points
- β’The Longest Increasing Subsequence (LIS) is the longest sequence you can extract from an array while keeping the original order and making each next element strictly larger.
- β’A simple dynamic programming solution runs in O() time and is easy to understand and reconstruct the sequence from.
- β’A faster O(n n) approach uses the patience sorting idea and binary search to maintain minimal possible tails for subsequences of each length.
- β’To reconstruct the actual LIS in O(n n), keep predecessor links and the index of the best tail for every length.
- β’Use lowe for strictly increasing LIS and uppe for non-decreasing LIS (allowing equal neighbors).
- β’The tails array stores the smallest ending value for any increasing subsequence of a given length, which guarantees optimal extension.
- β’Common pitfalls include confusing subsequences with subarrays and using the wrong bound (lower vs upper) when duplicates appear.
- β’LIS is a core subroutine for scheduling, versioning constraints, and 2D problems after sorting and reducing to 1D LIS.
Prerequisites
- βArrays and indexing β LIS is defined over sequences and uses random access into arrays.
- βBinary search β The O(n log n) method relies on lower_bound or upper_bound on a sorted structure.
- βBig-O notation β To compare O(n^2) DP with O(n \log n) patience-based algorithm.
- βDynamic programming basics β The O(n^2) recurrence and path reconstruction are classic DP patterns.
- βC++ STL algorithms and vectors β Implementations use std::vector, std::lower_bound, and std::upper_bound.
- βStable sorting and tie-breaking (optional) β Needed when reducing 2D problems to 1D LIS (e.g., sort by one key and tie-break by the other).
Detailed Explanation
Tap terms for definitions01Overview
The Longest Increasing Subsequence (LIS) problem asks: given an array of numbers, what is the longest subsequence (not necessarily contiguous) whose elements are strictly increasing? For example, in [3, 1, 2, 1, 8, 5, 6], one LIS is [1, 2, 5, 6] with length 4. The LIS is a classic problem because it embodies dynamic programming and greedy-plus-binary-search techniques, with both intuitive and efficient solutions. There are two main approaches. The first is an O(n^2) dynamic programming method where for each position i we compute dp[i], the best length ending at i, by looking back at all j < i with a[j] < a[i]. The second is an O(n \log n) method based on the patience sorting analogy: maintain an array tails such that tails[k] is the smallest possible tail of any increasing subsequence of length k+1, and update it using binary search. The LIS shows up in many disguises: as a building block in 2D problems (like envelopes nesting), as a way to measure disorder, and as a tool to enforce precedence constraints. Beyond just finding the length, we often need to reconstruct an actual sequence, which requires tracking predecessor indices along with the tails.
02Intuition & Analogies
Think of arranging a set of boxes on a shelf from left to right such that each box is taller than the one before it. You want the longest possible lineup while keeping their original left-to-right order. You cannot move boxes around; you can only choose which ones to keep. The O(n^2) way is like scanning every new box and asking, "Whatβs the longest lineup I can end with this box if I attach it to the best earlier box thatβs shorter?" This is slow but very straightforward. Now picture the patience sorting card game: you place cards into piles so that each pileβs top card is as small as possible for its pile height. The rule is: place a new card on the leftmost pile whose top is greater than or equal to it (for strictly increasing we use the first pile with top >= card, which is lower_bound). Each pile represents the tail value of some increasing subsequence length. Making a pileβs top as small as possible keeps your options open to grow longer sequences later. The number of piles at the end equals the LIS length. This idea leads to the O(n \log n) algorithm because finding the correct pile is a binary search. To actually retrieve the sequence, imagine attaching a "pointer" from every card to the card it extends; meanwhile, we also remember which card sits on top of each pile. After processing all cards, we follow the pointers from the top of the last pile backward to reconstruct the LIS. If you want to allow equal neighbors (non-decreasing subsequence), you change one rule: use the first pile whose top is strictly greater than the card (upper_bound), so equals stack together.
03Formal Definition
04When to Use
Use LIS when you need to preserve order while maximizing a monotonic property. Typical scenarios include: 1) Measuring how close a permutation is to being sorted (the longer the LIS, the fewer removals needed to make the array strictly increasing). 2) 2D problems that reduce to 1D LIS after sorting, such as Russian doll envelopes or longest chain of pairs; you sort by one coordinate and apply LIS (with careful tie-breaking) to the other. 3) Scheduling or dependency problems where tasks must follow an increasing key (e.g., start time, version number) and you want the longest feasible chain. 4) Competitive programming tasks that ask for either the LIS length, one LIS, or the number of LIS (the latter can be solved in O(n^2) DP or with segment trees/coordinate compression for faster solutions). 5) Streamed or large inputs where O(n^2) is too slow; the O(n \log n) algorithm is the right choice for n up to 2e5 or more. Choose the O(n^2) DP when n \le 2000 or when you prioritize simplicity and easy reconstruction. Choose O(n \log n) when n is large or time limits are tight. Use lower_bound for strictly increasing sequences and upper_bound for non-decreasing sequences (allow equals).
β οΈCommon Mistakes
Common pitfalls include: 1) Confusing subsequences with subarrays: subarrays are contiguous; subsequences are not. Always keep index order but you can skip elements. 2) Using the wrong binary search: for strictly increasing LIS you must use lower_bound (first \ge x); for non-decreasing you must use upper_bound (first > x). Using the wrong one changes the variant you compute. 3) Forgetting to track predecessors when you need to reconstruct the actual LIS in O(n \log n). Just storing the tails values is not enough; you need arrays of indices and a prev[] link. 4) Mishandling duplicates: with duplicates and lower_bound, equal values will replace the same-length tail (correct for strictly increasing), but if you expected equal neighbors to be allowed, switch to upper_bound. 5) Believing tails stores an actual subsequence: tails holds minimal tail values, not a valid LIS by itself. 6) Off-by-one errors in reconstruction: store the index of the last element for each length, and when you place an element at position p, set prev[current_index] = last_index_of_length_(p-1) if p > 0. 7) Overwriting tail indices without updating prev accordingly. 8) Assuming LIS is unique: there can be multiple LIS; reconstruction methods typically return one of them based on processing order.
Key Formulas
LIS DP Recurrence
Explanation: The best LIS ending at position i is 1 (just a[i]) plus the best LIS ending at some earlier j with a[j] < a[i]. If no such j exists, dp[i] = 1.
LIS Length from DP
Explanation: The overall LIS length is the largest dp[i] across all positions. It aggregates the per-position bests into the global answer.
Lower-bound Placement (Strict LIS)
Explanation: To insert into tails for strictly increasing LIS, find the first index with value greater than or equal to . Replace it with ; if p equals the current length, append.
Upper-bound Placement (Non-decreasing)
Explanation: For the non-decreasing variant, use the first index with value strictly greater than , effectively allowing equal neighbors to stay in the same length.
Tails Monotonicity
Explanation: The tails array is strictly increasing; otherwise a shorter tail could replace a longer-length tail. This monotonicity enables binary search and proves optimality.
Time Complexities
Explanation: The quadratic DP checks all prior elements for each i, while the patience-based method uses a binary search over tails per element. The latter scales better for large n.
Counting Number of LIS (DP)
Explanation: If also counting how many LIS end at i, sum counts of all predecessors that can extend to i. The total number of LIS is the sum of cnt[i] where dp[i] equals the LIS length.
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Reads n and array, prints LIS length and one LIS using O(n^2) DP 5 int main() { 6 ios::sync_with_stdio(false); 7 cin.tie(nullptr); 8 9 int n; 10 if (!(cin >> n)) return 0; 11 vector<long long> a(n); 12 for (int i = 0; i < n; ++i) cin >> a[i]; 13 14 if (n == 0) { 15 cout << 0 << "\n\n"; 16 return 0; 17 } 18 19 vector<int> dp(n, 1); // dp[i] = LIS length ending at i 20 vector<int> prev(n, -1); // prev[i] = predecessor index of i in the LIS 21 22 int bestLen = 1, bestEnd = 0; 23 for (int i = 0; i < n; ++i) { 24 for (int j = 0; j < i; ++j) { 25 if (a[j] < a[i] && dp[j] + 1 > dp[i]) { 26 dp[i] = dp[j] + 1; 27 prev[i] = j; 28 } 29 } 30 if (dp[i] > bestLen) { 31 bestLen = dp[i]; 32 bestEnd = i; 33 } 34 } 35 36 // Reconstruct LIS by walking predecessors from bestEnd 37 vector<long long> lis; 38 for (int i = bestEnd; i != -1; i = prev[i]) lis.push_back(a[i]); 39 reverse(lis.begin(), lis.end()); 40 41 cout << bestLen << "\n"; 42 for (int i = 0; i < (int)lis.size(); ++i) { 43 if (i) cout << ' '; 44 cout << lis[i]; 45 } 46 cout << "\n"; 47 return 0; 48 } 49
This quadratic DP checks all previous elements j for each i to decide whether appending a[i] to the best subsequence ending at j improves dp[i]. It also records predecessors to reconstruct an actual LIS at the end.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Reads n and array, prints the length of the strictly increasing LIS using tails + lower_bound 5 int main() { 6 ios::sync_with_stdio(false); 7 cin.tie(nullptr); 8 9 int n; 10 if (!(cin >> n)) return 0; 11 vector<long long> a(n); 12 for (int i = 0; i < n; ++i) cin >> a[i]; 13 14 vector<long long> tails; // tails[k] = minimal tail value of length k+1 15 tails.reserve(n); 16 17 for (long long x : a) { 18 // For strictly increasing LIS, use lower_bound: first element >= x 19 auto it = lower_bound(tails.begin(), tails.end(), x); 20 if (it == tails.end()) tails.push_back(x); 21 else *it = x; 22 } 23 24 cout << (int)tails.size() << "\n"; 25 return 0; 26 } 27
Maintains the minimal tail for each length and uses lower_bound to find where the current value fits. The size of tails at the end equals the LIS length.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Reconstructs one strictly increasing LIS in O(n log n) 5 int main() { 6 ios::sync_with_stdio(false); 7 cin.tie(nullptr); 8 9 int n; 10 if (!(cin >> n)) return 0; 11 vector<long long> a(n); 12 for (int i = 0; i < n; ++i) cin >> a[i]; 13 14 if (n == 0) { 15 cout << 0 << "\n\n"; 16 return 0; 17 } 18 19 // tailsVal[ell] = minimal tail value for length ell+1 20 // tailsIdx[ell] = index in a[] of the element providing that tail 21 vector<long long> tailsVal; 22 vector<int> tailsIdx; 23 tailsVal.reserve(n); 24 tailsIdx.reserve(n); 25 26 // prev[i] = index of predecessor of a[i] in the LIS ending at i 27 vector<int> prev(n, -1); 28 29 for (int i = 0; i < n; ++i) { 30 long long x = a[i]; 31 // Strictly increasing: lower_bound (first >= x) 32 int p = int(lower_bound(tailsVal.begin(), tailsVal.end(), x) - tailsVal.begin()); 33 if (p == (int)tailsVal.size()) { 34 tailsVal.push_back(x); 35 tailsIdx.push_back(i); 36 } else { 37 tailsVal[p] = x; 38 tailsIdx[p] = i; 39 } 40 // Link to the last index of subsequence length p (which is at length p) 41 if (p > 0) prev[i] = tailsIdx[p - 1]; 42 } 43 44 int L = (int)tailsVal.size(); 45 // Reconstruct by starting from the last index of length L 46 int cur = tailsIdx[L - 1]; 47 vector<long long> lis; 48 lis.reserve(L); 49 while (cur != -1) { 50 lis.push_back(a[cur]); 51 cur = prev[cur]; 52 } 53 reverse(lis.begin(), lis.end()); 54 55 cout << L << "\n"; 56 for (int i = 0; i < (int)lis.size(); ++i) { 57 if (i) cout << ' '; 58 cout << lis[i]; 59 } 60 cout << "\n"; 61 return 0; 62 } 63
This version keeps, for every length, the index of the array element that is currently the best tail. Each element i links to the last index used for length p (prev[i] = tailsIdx[p-1]). At the end, following prev from the best tail reconstructs the LIS.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Returns length of Longest Non-Decreasing Subsequence (allows equals) 5 int main() { 6 ios::sync_with_stdio(false); 7 cin.tie(nullptr); 8 9 int n; 10 if (!(cin >> n)) return 0; 11 vector<long long> a(n); 12 for (int i = 0; i < n; ++i) cin >> a[i]; 13 14 vector<long long> tails; // minimal tail for each non-decreasing length 15 tails.reserve(n); 16 17 for (long long x : a) { 18 // Non-decreasing: place x after all <= x, i.e., first > x 19 auto it = upper_bound(tails.begin(), tails.end(), x); 20 if (it == tails.end()) tails.push_back(x); 21 else *it = x; 22 } 23 24 cout << (int)tails.size() << "\n"; 25 return 0; 26 } 27
Switching to upper_bound changes the rule to allow equal neighbors by placing x after all values <= x. This computes the longest non-decreasing subsequence length.