Find the length of the longest strictly increasing subsequence, then get it under quadratic time.
The quadratic dynamic programme asks for the best chain ending at each index. The faster version keeps one array where slot j holds the smallest possible tail of an increasing subsequence of length j plus one; that array stays sorted, so each element is placed by binary search for O of n log n.
What the interviewer is scoring
- Whether the candidate can state what a slot of the tails array means, in one sentence, before coding it
- That the tails array is not claimed to be a subsequence of the input
- Does the candidate justify lower bound versus upper bound from the strict-versus-non-decreasing wording
- Whether the monotonicity of the tails array is argued rather than assumed, since the binary search depends on it
- Does the candidate offer the quadratic version first and only then improve it, rather than reaching straight for the trick
Answer
Start with the quadratic recurrence
Define dp[i] as the length of the longest increasing subsequence that ends at index i. Anchoring the state to an endpoint rather than to a prefix is the move that makes the recurrence expressible, because an increasing subsequence can only be extended by comparing against its final element, and a state that says merely "the best answer among the first i elements" does not record what that final element was.
The transition scans backwards: dp[i] = 1 + max(dp[j]) over every j < i with nums[j] < nums[i], or 1 if no such j exists. The answer is the maximum over the whole array, not dp[n-1], since the longest chain need not end at the last element. That is O(n²) time and O(n) space, and it is a complete, correct answer that you should write down and offer before optimising. Interviewers do ask for the faster version, but starting there and getting the invariant slightly wrong is a much worse outcome than arriving at it in two steps.
What the faster version actually stores
Keep an array tails, where tails[j] is the smallest value that can end an increasing subsequence of length j + 1 among the elements processed so far. size is how many slots are in use, which is the best length found so far.
Two consequences follow, and both matter. First, tails is strictly increasing: a chain of length four ends no lower than the best chain of length three, because dropping the last element of the four-chain gives a three-chain with a smaller tail. That monotonicity is what licenses a binary search over it, and it is the claim to state out loud, because without it the algorithm is unexplained.
Second, preferring the smallest possible tail is never a mistake. A lower tail accepts every future element a higher tail would accept, and possibly more, so replacing a tail can only improve your options for extending that length later. It cannot shorten anything already found, because the lengths recorded in the other slots are untouched.
So each new element does exactly one of two things. If it exceeds every tail, it extends the longest chain and occupies a new slot. Otherwise it replaces the smallest tail that is greater than or equal to it, improving the prospects for that length without changing any length.
int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int size = 0;
for (int x : nums) {
int lo = 0, hi = size;
while (lo < hi) { // lower bound: first slot whose tail is >= x
int mid = (lo + hi) >>> 1;
if (tails[mid] < x) lo = mid + 1;
else hi = mid;
}
tails[lo] = x; // replace that tail, or write into a fresh slot
if (lo == size) size++; // lo == size means x beat every existing tail
}
return size;
}
Time is O(n log n): one binary search over at most size slots per element. Space is O(n) for the tails array, and in practice it is only ever size entries deep.
Note the search is written by hand. Arrays.binarySearch returns an insertion point only when the key is absent, and its behaviour with duplicate keys is explicitly unspecified — the Javadoc says there is no guarantee which of several equal elements is found. Since duplicates are precisely the case that decides strict versus non-decreasing here, borrowing the library method makes the one interesting branch depend on unspecified behaviour.
The claim that trips people up
tails is not a subsequence of the input and printing it is wrong. It is a set of independent best-known endpoints, assembled from elements that may appear in any order and may never have coexisted in a single chain. Take [10, 9, 2, 5, 3, 7, 101, 18]. The answer is length 4, and [2, 3, 7, 18] is one valid subsequence — but at the end of the loop tails holds [2, 3, 7, 18] only by coincidence of this input. With [4, 5, 6, 1] the final contents are [1, 5, 6], and 1 follows 6 in the original array, so those three values are not a chain at all. The length is right; the array is not the answer.
That is why recovering the subsequence needs extra bookkeeping. Record which input index landed in each slot, and for each element the index that occupied the slot below it at the time — that is a legitimate predecessor, because it ends a chain one shorter with a value strictly smaller.
int n = nums.length;
int[] tails = new int[n], tailIdx = new int[n], prev = new int[n];
int size = 0;
for (int i = 0; i < n; i++) {
int lo = 0, hi = size;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (tails[mid] < nums[i]) lo = mid + 1;
else hi = mid;
}
tails[lo] = nums[i];
tailIdx[lo] = i;
prev[i] = lo > 0 ? tailIdx[lo - 1] : -1; // whoever ends the next-shorter chain right now
if (lo == size) size++;
}
// Walk backwards from tailIdx[size - 1] through prev, then reverse, to get one longest subsequence.
There is usually more than one longest subsequence, so this reconstructs a longest one. If the interviewer wants a specific tie-break, that is a different problem and worth clarifying rather than assuming yours matches.
Strict versus non-decreasing is one comparison
The wording decides the search predicate, and both are one character apart. For a strictly increasing subsequence you want the lower bound — the first slot whose tail is greater than or equal to x — so an equal value replaces rather than extends, which correctly refuses to chain two identical numbers. For a non-decreasing subsequence you want the upper bound, the first slot strictly greater than x, so equal values do extend. In the loop above that is the difference between tails[mid] < x and tails[mid] <= x.
Getting this backwards produces answers that are off by exactly the number of runs of duplicates, which passes any test input with distinct values. If the problem statement says "increasing" without qualification, ask which one is meant instead of guessing, and then say which comparator you chose and why.
Where else this shape appears
The technique is patience sorting: each slot is the top card of a pile, a new card goes on the leftmost pile whose top it can cover, and the number of piles is the answer. Recognising that is what lets you spot the problem when it arrives disguised. Nesting one rectangle inside another, stacking envelopes, wiring pairs of ports without crossings, finding the minimum number of arrows for a set of intervals — all of these reduce to sorting on one dimension and running this on the other. The reduction is where the difficulty lives, and the tie-break in the sort is where those reductions go wrong: if two items share the first coordinate and equality is not allowed in the answer, they must be ordered so that they cannot chain, which usually means sorting the second coordinate descending within a tie.
Likely follow-ups
- Return the subsequence itself, not just its length.
- What single character changes if the subsequence may be non-decreasing?
- Why does Arrays.binarySearch not give you the index you need here?
- How many pairs of numbers can you nest inside each other, given a list of width and height pairs?
Related questions
- Given package weights in a fixed order, find the smallest ship capacity that delivers them all within D daysmediumAlso on binary-search5 min
- How do you know a problem is dynamic programming?mediumAlso on dynamic-programming5 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: coding7 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardSame kind of round: coding6 min
- How do you decide between BFS and DFS - and how do you recognise a graph problem that nobody described as a graph?mediumSame kind of round: coding4 min
- Design a parking lot system — you have 90 minutes, working code at the end.hardSame kind of round: coding5 min
- How do goroutines leak, and how would you find a leak in a running service?mediumSame kind of round: coding5 min
- Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?hardSame kind of round: coding4 min