Longest Increasing Subsequence

Keep the smallest possible tail for each subsequence length.

Approach

The O(n²) DP sets dp[i] to the best subsequence ending at i. The elegant O(n log n) approach maintains a 'tails' array where tails[k] is the smallest tail of an increasing subsequence of length k+1; binary search each number's insertion point. The tails length is the answer.

Time complexity

O(n log n)

Space complexity

O(n)

Common mistake

Thinking the tails array is an actual subsequence — it isn't, but its length is correct.

Interactive walkthrough — step through it below, free and without an account

Problem

Given an array of integers, determine the maximum number of elements that can form a strictly increasing subsequence while preserving their original order. The chosen elements do not need to be adjacent, and only the length of the best subsequence is required.

Examples

Input: nums = [10,9,2,5,3,7,101,18]

Output: 4

One longest increasing subsequence is [2,3,7,101]. The subsequence [2,3,7,18] also has length 4.

Input: nums = [0,1,0,3,2,3]

Output: 4

A longest increasing subsequence is [0,1,2,3].

Problem

Problem

Given an array of integers, determine the maximum number of elements that can form a strictly increasing subsequence while preserving their original order. The chosen elements do not need to be adjacent, and only the length of the best subsequence is required.

Problem and Goal

Learning goal: Identify what must be optimized when selecting an increasing subsequence.

Core idea A subsequence keeps the original left-to-right order, but it may skip elements. We want the largest number of selected values such that every next value is strictly greater than the previous one. The answer is a length, not the subsequence itself.

Algorithm plan

  • Scan the array from left to right.
  • Decide whether each number can extend an increasing sequence.
  • Preserve enough information about earlier choices to make the best future decisions.
  • Return the maximum achievable length.

Micro-example For [10,9,2,5,3,7,101,18], the values [2,3,7,101] form an increasing subsequence. The answer is therefore 4, even though the selected values are not contiguous.

Pitfalls

  • A subsequence is not required to occupy consecutive positions.
  • Increasing means strictly increasing, so equal values cannot extend a sequence.
  • The goal is length; returning the actual values requires extra reconstruction logic.

Recap

  • Preserve order while allowing skips.
  • Maximize the number of selected values.
  • The sample answer is 4.

Problem and Goal

The target

From `[10,9,2,5,3,7,101,18]`, select values in their original order so each selected value is larger than the previous one.

The longest valid selection has length `4`, such as `[2,3,7,101]`.

The Smallest-Tail Invariant

Learning goal: Understand why keeping the smallest possible tail makes longer subsequences easier to build.

Core idea For every achievable length, keep the smallest ending value seen so far. A smaller tail is more flexible: it leaves more room for a future number to extend the sequence. We do not need to remember every sequence, only the best tail for each length.

Key insight

For the same subsequence length, a smaller tail dominates a larger tail because every future extension that works after the larger tail also works after the smaller one.

Data structure model

  • tails[k] is the smallest tail of any increasing subsequence of length k + 1.
  • tails is always sorted in strictly increasing order.
  • For each incoming number, binary search for the first tail greater than or equal to it.
  • Replace that tail, or append if the number is larger than every tail.

Micro-example After processing 2,5,3, the tails array is [2,3]. The length-two sequence ending at 3 is more promising than one ending at 5.

Pitfalls

  • tails stores optimal tail values, not necessarily values from one real subsequence.
  • Use the first index with tails[index] >= x; this is lower bound.
  • Equal values replace a tail rather than increasing the answer.

Recap

  • Smaller tails create more extension opportunities.
  • Binary search finds the correct tail to replace.
  • The length of tails is the answer.

The Smallest-Tail Invariant

Replace a tail when the new value is smaller or equal.

`[2,5]` becomes `[2,3]` after reading `3`.

From O(n²) DP to O(n log n)

Learning goal: Connect the direct dynamic programming solution to the binary-search optimization.

Core idea The O(n²) method defines dp[i] as the longest increasing subsequence ending exactly at index i. For every earlier index j, we can extend dp[j] when nums[j] < nums[i]. This is clear but compares many pairs.

Recurrence

dp[i] = 1 + max(dp[j]) over all j < i with nums[j] < nums[i].

The optimized method groups states by subsequence length instead of by ending index. Because tails is sorted, lower-bound binary search replaces a linear scan.

Algorithm plan

  • Start with an empty tails array.
  • For each x, find the first position k where tails[k] >= x.
  • Replace tails[k] with x; if no such position exists, append x.
  • Return len(tails).

Micro-example Reading 2,5,3 produces [2], then [2,5], then [2,3]. The length stays 2, but the tail becomes easier to extend.

Pitfalls

  • The optimized method does not calculate every dp[i] value.
  • Binary search must use lower bound for a strictly increasing subsequence.
  • Replacing a tail preserves the best length so far; it does not reduce the answer.

Recap

  • DP is indexed by ending position.
  • tails is indexed by subsequence length.
  • Sorted tails enable O(log n) updates.

From O(n²) DP to O(n log n)

ApproachStateTimeKey trade-off
O(n²) DPBest sequence ending at index iO(n^2)Straightforward recurrence
Tails + binary searchSmallest tail for each lengthO(n log n)More subtle invariant; length is correct

End-to-End Walkthrough

Learning goal: Trace lower-bound insertion on one complete input and see how tails evolves.

Core idea Each number asks one question: where is the first tail that is at least this number? Replacing there keeps the same subsequence length alive with a better ending value. Appending means the number extends every known length and creates a new record.

Algorithm plan

  • Process [10,9,2,5,3,7,101,18] from left to right.
  • Binary search the current sorted tails array.
  • Replace the first value greater than or equal to the incoming number.
  • Read the final array length as the result.

Micro-example After processing 2,5,3,7, the state is tails = [2,3,7]. When 101 arrives, it is larger than all tails, so it appends and creates length 4. When 18 arrives, it replaces 101; the length remains 4.

Pitfalls

  • Replacing 101 with 18 does not erase the known length-four possibility.
  • A replacement is not an extension unless it occurs beyond the current last index.
  • The binary-search range is always the current tails, not the original input.

Recap

  • Process each value once.
  • Search and update a sorted tail summary.
  • Final tails length is 4 for this example.

End-to-End Walkthrough

Traces the optimized algorithm on the example [10,9,2,5,3,7,101,18]. Each event shows one lower-bound comparison and the resulting tails state.

Binary search
1 / 19
Read 10: the first value starts a length-one tail.

Array

10mid
lo=0hi=0mid=0target=10check: mid=10 = target

Action: initialize tails

The first number always creates one subsequence.tails is sorted and stores the smallest known tail per length.
1 / 19

Pitfalls and Edge Cases

Learning goal: Avoid the implementation mistakes that invalidate the tails method.

Core idea The algorithm is compact because its invariant carries a lot of meaning. The most important distinction is that tails is a summary of best possible endings by length, not a record of one chosen path through the input. Its length is correct even when adjacent entries came from incompatible positions.

Algorithm plan

  • Use lower bound: find the first tails[k] >= x.
  • Replace that slot, or append when x exceeds every tail.
  • Keep the array sorted after every operation.
  • Treat duplicates as replacements, not extensions.

Common pitfall

Do not assume the tails array is an actual increasing subsequence from the input. It is not guaranteed to be reconstructible as one sequence; only its length is guaranteed to equal the LIS length.

Micro-example For input [2,5,3], the final tails is [2,3]. This happens to be a valid subsequence here, but that coincidence is not required by the invariant.

Pitfalls

  • Using upper bound (> x) would allow equal values to extend the sequence.
  • Returning the last tail value instead of tails.length answers the wrong question.
  • Forgetting to process values in input order changes the problem.

Recap

  • Lower bound enforces strict increase.
  • tails is not necessarily a real subsequence.
  • Only its length represents the answer.

Pitfalls and Edge Cases

What does `tails = [2,3,7,18]` guarantee after processing the input?

Complexity and Wrap-up

Learning goal: Explain why the optimized solution meets the target bounds and recognize when to use it.

Core idea There are n input values. Each value performs one binary search over the sorted tails array, taking O(log n) time, followed by a replacement or append. The array uses O(n) space in the worst case, such as when the input is already increasing.

Algorithm plan

  • Maintain tails as a sorted list.
  • For each x, binary search its lower-bound position.
  • Replace the first tail at least x, or append x.
  • Return tails.length.

Micro-example For [10,9,2,5,3,7,101,18], the final state is [2,3,7,18]. The algorithm performs eight searches and returns 4.

Pitfalls

  • The O(n²) DP is useful for learning and may be acceptable for smaller inputs, but it misses the target O(n log n) performance.
  • Binary search is valid only because tails remains sorted.
  • The optimized array gives the length directly; reconstructing the sequence needs predecessor metadata.

Recap

  • Time: O(n log n).
  • Space: O(n).
  • Smallest tails preserve the correct LIS length without storing every subsequence.

Complexity and Wrap-up

Shows the cost of processing each number with lower-bound binary search.

Complexity analysis
Time
O(n log n)
Space
O(n)
Where the time goes
  • Process n valuesEach input value is handled once.O(n)
  • Binary search insertion pointtails is sorted, so lower bound is logarithmic.O(log n)
  • Store tailsThe array can grow to length n in an increasing input.O(n)
How it grows
input size n →opsO(n²)O(n log n)O(n)O(log n)O(1)
Roughly what that costs
n101001,00010,000
O(n log n)3366410K132.9K
O(n)101001K10K

The optimized method meets the target by replacing a linear scan of prior states with binary search.

Now try it on your own problem

Paste any LeetCode problem — or one of your own — and codebrew builds a walkthrough like this one for it.

Build a lesson from any problem

Related problems