Topics › Dynamic Programming › DP on LIS
Print Longest Increasing Subsequence
Java source ↗ Edit this article ↗
Jump to code:
Problem
Given an integer array a, return the elements of a longest strictly increasing subsequence. Usually several subsequences have the maximum length, so the problem fixes which one to return: the index-wise lexicographically smallest one. Compare two candidates by the array indices they use; at the first position where they differ, the one using the earlier index wins.
Constraints: 1 ≤ n ≤ 1000 in the usual version; the Java file also returns an empty list for an empty array.
Example 1
a = [10, 9, 2, 5, 3, 7, 101, 18]
answer = [2, 5, 7, 101]
There are four LIS of length 4: 2,5,7,101, 2,5,7,18, 2,3,7,101, 2,3,7,18. Their index lists are (2,3,5,6), (2,3,5,7), (2,4,5,6), (2,4,5,7); the first is smallest.
Example 2
a = [5, 4, 3]
answer = [5] // every single element is an LIS of length 1; index 0 is the earliest
Knowing the length is not enough here: we must reconstruct an actual subsequence. The standard tool is the parent pointer: while computing the DP, remember which earlier element each best answer came from, then walk the pointers back.
Approach 1: Brute force recursion
Intuition. Generalise the take-or-skip recursion for the LIS length so that it returns the best subsequence itself (as a list of indices). At index i with last taken index prev:
skip= best answer fromi + 1with the sameprev.- If
a[i]can be taken (prev == -1ora[i] > a[prev]),take=ifollowed by the best answer fromi + 1withprev = i. - Return the longer one. On a tie, return
take: it places indexiat the current position, while every index inskipis larger thani, sotakeis lexicographically smaller.
Because each sub-result is itself the smallest for its subproblem, gluing i in front keeps the result the smallest overall.
static List<Integer> bruteForce(int[] a) {
List<Integer> res = new ArrayList<>();
for (int i : best(0, -1, a)) res.add(a[i]);
return res;
}
static List<Integer> best(int i, int prev, int[] a) {
if (i == a.length) return new ArrayList<>();
List<Integer> skip = best(i + 1, prev, a);
if (prev != -1 && a[i] <= a[prev]) return skip; // a[i] cannot be taken
List<Integer> take = best(i + 1, i, a);
take.add(0, i);
return take.size() >= skip.size() ? take : skip; // on a tie, take puts the smaller index first
}
Complexity. Time O(2^n · n): every take-or-skip choice is explored and lists are copied. Space O(n) stack (plus the lists). Useful as a reference for testing only.
Approach 2: DP with parent pointers
Intuition. Use the “LIS ending at i” DP: dp[i] is the length of the longest increasing subsequence ending at index i. While computing dp[i] = 1 + max(dp[j]) over j < i with a[j] < a[i], also store parent[i] = j for the j that achieved the maximum. Initialise parent[i] = i to mean “this subsequence starts here”.
At the end, let last be an index with the maximum dp. Following parent from last until an index points to itself visits the subsequence backwards; reversing gives the answer.
Which LIS does this return? Two choices make it the index-wise smallest one:
lastis the first index with the maximum length (we update only on a strictly largerdp).parent[i]is the smallestjthat achieves the maximum (the inner loop runs left to right and updates only on a strict improvement).
Why that is enough rests on one fact: if i < i' have the same dp value, then a[i] ≥ a[i']. (If a[i] < a[i'], appending a[i'] to the best subsequence ending at i would give dp[i'] ≥ dp[i] + 1.) Also, the element at position k of any LIS has dp exactly k; a larger value would let us lengthen the LIS. Now compare our answer s with the true smallest answer b, from the back. s_L ≤ b_L because s_L is the first index of maximum length. If s_{k+1} ≤ b_{k+1}, then a[b_k] < a[b_{k+1}] ≤ a[s_{k+1}] by the fact above, so b_k (if it lies before s_{k+1}) was a valid parent candidate and s_k ≤ b_k; otherwise s_k < s_{k+1} ≤ b_k anyway. So s is position-by-position no larger than b, and since b is the smallest, s = b.
Algorithm.
- For each
i:dp[i] = 1,parent[i] = i; for eachj < iwitha[j] < a[i]anddp[j] + 1 > dp[i], setdp[i] = dp[j] + 1,parent[i] = j. Tracklast. - Walk from
lastthroughparentuntil a self-loop, collecting values; reverse.
static List<Integer> dpWithParent(int[] a) {
int n = a.length;
List<Integer> res = new ArrayList<>();
if (n == 0) return res;
int[] dp = new int[n], parent = new int[n];
int last = 0; // first index where the maximum length ends
for (int i = 0; i < n; i++) {
dp[i] = 1;
parent[i] = i; // i points to itself: start of a subsequence
for (int j = 0; j < i; j++) {
if (a[j] < a[i] && dp[j] + 1 > dp[i]) { // strict '>' keeps the smallest such j
dp[i] = dp[j] + 1;
parent[i] = j;
}
}
if (dp[i] > dp[last]) last = i;
}
res.add(a[last]);
while (parent[last] != last) {
last = parent[last];
res.add(a[last]);
}
Collections.reverse(res);
return res;
}
Dry run on a = [10, 9, 2, 5, 3, 7, 101, 18]:
| i | a[i] | dp[i] | parent[i] |
|---|---|---|---|
| 0 | 10 | 1 | 0 |
| 1 | 9 | 1 | 1 |
| 2 | 2 | 1 | 2 |
| 3 | 5 | 2 | 2 |
| 4 | 3 | 2 | 2 |
| 5 | 7 | 3 | 3 (first index with dp 2 and a smaller value) |
| 6 | 101 | 4 | 5 |
| 7 | 18 | 4 | 5 |
last = 6; walking back gives 6 → 5 → 3 → 2, so the answer is [2, 5, 7, 101].
Complexity. Time O(n^2), space O(n). This is the expected interview solution.
Approach 3: Optimal, binary search with levels
Intuition. The O(n log n) tails method computes dp[i] for free: when a[i] lands at position pos of the tails array, the longest increasing subsequence ending at i has length pos + 1. Group the indices by that length into levels, each kept in index order. By the fact proved above, values inside one level never increase from left to right.
Rebuild exactly the subsequence that Approach 2 finds:
- The last element is the first index in the top level.
- Going down one level, Approach 2’s parent is the smallest index in that level with a value below the current element (it is automatically before the current element). Because values in a level are non-increasing, the indices with a smaller value form a suffix of the level, so the first of them is found by binary search.
static List<Integer> binarySearchLevels(int[] a) {
int n = a.length;
int[] tails = new int[n]; // tails[k] = smallest last value of an increasing subsequence of length k + 1
List<List<Integer>> levels = new ArrayList<>(); // levels.get(k) = indices with dp = k + 1, in index order
int len = 0;
for (int i = 0; i < n; i++) {
int lo = 0, hi = len; // first k with tails[k] >= a[i]
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (tails[mid] < a[i]) lo = mid + 1; else hi = mid;
}
tails[lo] = a[i];
if (lo == len) {
len++;
levels.add(new ArrayList<>());
}
levels.get(lo).add(i); // dp[i] = lo + 1
}
List<Integer> res = new ArrayList<>();
if (len == 0) return res;
int cur = levels.get(len - 1).get(0); // first index with the maximum length
res.add(a[cur]);
for (int k = len - 2; k >= 0; k--) {
List<Integer> level = levels.get(k); // values here never increase left to right
int lo = 0, hi = level.size() - 1; // first position whose value is below a[cur]
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (a[level.get(mid)] < a[cur]) hi = mid; else lo = mid + 1;
}
cur = level.get(lo);
res.add(a[cur]);
}
Collections.reverse(res);
return res;
}
Complexity. Time O(n log n): one binary search per element while building, one per level while rebuilding. Space O(n).
Edge cases and pitfalls
- Strict comparisons. Use
a[j] < a[i]anddp[j] + 1 > dp[i]. Writing>=in the second test picks the last best parent and returns a different LIS, which fails when the expected output is fixed. - Choosing
last. Take the first index with the maximumdp, not the last one. - Self-loop as terminator.
parent[i] = imarks the start; a separate-1value also works but must be handled when walking. - Reverse at the end. The walk produces the subsequence backwards.
tailsis not the answer. In the binary search method,tailsis not a valid subsequence in general; that is why levels (or parent pointers) are needed.- Duplicates.
[3, 3, 3]returns[3], the element at index0. - Empty input. Return an empty list rather than indexing
dp[0].
Related problems
- Longest Increasing Subsequence computes only the length, with all DP formulations.
- Longest Increasing Subsequence - (DP-43) explains the tails array used in Approach 3.
- Largest Divisible Subset prints a chain with exactly the same parent-pointer technique.
- Print Longest Common Subsequence - (DP - 26) reconstructs an answer by walking a DP table backwards.