Longest Increasing Subsequence

Problem

Given an integer array nums, return the length of the longest strictly increasing subsequence.

  • 1 ≤ nums.length ≤ 2500
  • −10⁴ ≤ nums[i] ≤ 10⁴

Example

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

The longest increasing subsequence is [2,3,7,101], which has length 4. The algorithm uses recursion with memoization to compute the LIS ending at each index. For example, at index 6 (value 101), it checks all previous indices with smaller values (like 7 at index 5) and builds upon their LIS lengths. The critical moment is recognizing that the LIS ending at index 6 is 1 plus the maximum LIS among all indices with smaller values before it. Memoization avoids redundant recomputation by caching results for each index.

Approach

Straightforward Solution

A brute-force approach tries all subsequences, which is exponential in time and infeasible for large inputs.

Core Observation

The length of the longest increasing subsequence ending at position i depends on the lengths of all increasing subsequences ending at positions before i with smaller values. This exhibits optimal substructure and overlapping subproblems, making it a natural fit for dynamic programming.

Path to Optimal

Recognizing the problem's optimal substructure allows defining a recursive function dp(i) that returns the LIS length ending at index i. For each i, dp(i) is 1 plus the maximum dp(j) for all j < i where nums[j] < nums[i]. Memoization caches these results to avoid recomputation, reducing the complexity drastically.

Optimal Approach

Implement a top-down recursive DP with memoization. For each index i, compute dp(i) by checking all previous indices j < i where nums[j] < nums[i], and take the maximum dp(j) + 1. The final answer is the maximum dp(i) over all indices. This approach runs in O(n^2) time and O(n) space.

Time

O(n^2)

For each index i, the algorithm checks all previous indices j < i, resulting in a nested loop behavior. Memoization ensures each dp(i) is computed once, leading to O(n^2) total computations.

Space

O(n)

The memo dictionary stores one entry per index, and the recursion stack depth is at most n, resulting in O(n) auxiliary space.

Pattern Spotlight

Dynamic Programming (Top-Down Memoization)

When a problem asks for an optimal subsequence or substructure that depends on previous states, define a recursive function representing the solution for a subproblem and cache results to avoid redundant work, transforming exponential recursion into polynomial time.

Solution

1class Solution:
2 def lengthOfLIS(self, nums: list[int]) -> int:
3 memo = {}
4
5 def dp(i):
6 if i in memo:
7 return memo[i]
8
9 max_len = 1
10 for j in range(i):
11 if nums[i] > nums[j]:
12 max_len = max(max_len, 1 + dp(j))
13
14 memo[i] = max_len
15 return max_len
16
17 if not nums:
18 return 0
19 return max(dp(i) for i in range(len(nums)))

Step-by-Step Solution

1

Handle Empty Input Edge Case

17if not nums:
18 return 0

Objective

To immediately return 0 if the input array is empty, as no subsequence exists.

Key Insight

An empty input means there are no elements to form any subsequence, so the longest increasing subsequence length is zero. Handling this edge case upfront prevents unnecessary recursion and potential errors.

Interview Quick-Check

State & Boundaries

Check if nums is empty before any processing to handle trivial input cases correctly.

2

Compute LIS Lengths Using Memoized Recursion

3memo = {}
5def dp(i):
6 if i in memo:
7 return memo[i]
9 max_len = 1
10 for j in range(i):
11 if nums[i] > nums[j]:
12 max_len = max(max_len, 1 + dp(j))
14 memo[i] = max_len
15 return max_len

Objective

To recursively compute the length of the longest increasing subsequence ending at each index, caching results to avoid redundant work.

Key Insight

The recursive function dp(i) captures the LIS length ending at index i by exploring all previous indices j < i where nums[j] < nums[i]. By taking the maximum dp(j) + 1, it builds the solution from smaller subproblems. Memoization ensures each dp(i) is computed once, converting exponential recursion into efficient polynomial time.

Interview Quick-Check

Core Logic

The dp(i) function returns the LIS length ending at i by checking all j < i with nums[j] < nums[i], taking the max dp(j) + 1, and memoizing results.

State & Boundaries

Memoization cache prevents recomputation of dp(i) for the same index, ensuring O(n^2) time instead of exponential.

Common Pitfalls & Bugs

Forgetting to memoize results leads to exponential time due to repeated subproblem evaluations.

3

Aggregate Final Result from All Subproblems

19return max(dp(i) for i in range(len(nums)))

Objective

To find the overall longest increasing subsequence length by taking the maximum dp(i) over all indices.

Key Insight

Since the LIS can end at any index, the final answer is the maximum value among all dp(i). The generator expression efficiently computes dp(i) for all i and returns the maximum, leveraging memoization to avoid redundant computations.

Interview Quick-Check

Core Logic

The final LIS length is the maximum dp(i) for i in [0, n-1], as the subsequence can end anywhere.

Complexity

Computing dp(i) for all i with memoization ensures each subproblem is solved once, maintaining O(n^2) time.

Line Analysis

This solution has 4 Critical lines interviewers watch for.

Line 6 Critical
if i in memo:

Return cached result if dp(i) has been computed before.

This memoization check prevents repeated work and is the key optimization that reduces time complexity from exponential to polynomial.

Line 12 Critical
max_len = max(max_len, 1 + dp(j))

Update max_len by considering the LIS ending at j plus nums[i].

This line embodies the core DP recurrence: the LIS ending at i is 1 plus the maximum LIS ending at any smaller previous element, capturing optimal substructure.

Line 7 Critical
return memo[i]

Return the cached LIS length for index i.

Returning the cached value immediately avoids unnecessary recursion and ensures correctness by reusing previously computed results.

Line 14 Critical
memo[i] = max_len

Cache the computed LIS length for index i.

Storing the result in memo ensures that future calls to dp(i) return instantly, preventing recomputation and enabling efficient top-down DP.

Line 3 Core
memo = {}

Initialize a memoization dictionary to cache LIS lengths for each index.

Memoization is essential to avoid redundant recursive calls, transforming exponential recursion into polynomial time by storing results of subproblems.

Line 11 Core
if nums[i] > nums[j]:

Check if nums[i] can extend the subsequence ending at nums[j].

Only elements smaller than nums[i] can be predecessors in an increasing subsequence, ensuring strict increase.

Line 19 Core
return max(dp(i) for i in range(len(nums)))

Return the maximum LIS length over all indices in nums.

Since the LIS can end at any index, taking the maximum dp(i) over all i yields the overall longest increasing subsequence length.

Line 5 Core
def dp(i):

Define the recursive function dp that computes LIS length ending at index i.

Encapsulating the LIS computation in a recursive function with memoization captures the problem's optimal substructure and overlapping subproblems cleanly.

Line 9 Core
max_len = 1

Initialize max_len to 1, representing the LIS length including only nums[i].

Every element alone forms an increasing subsequence of length 1, serving as the base case for the recursion.

Line 15 Core
return max_len

Return the LIS length ending at index i.

Returning the computed value completes the recursive step, allowing the caller to build upon this result.

Line 10 Standard
for j in range(i):

Iterate over all indices j before i to find smaller elements.

Checking all previous indices allows building the LIS ending at i by extending smaller subsequences found earlier.

Line 17 Standard
if not nums:

Check if the input array is empty.

Handling empty input early prevents errors and unnecessary computation, as no subsequence exists in an empty array.

Line 18 Standard
return 0

Return 0 for empty input, indicating no subsequence.

The longest increasing subsequence length for an empty array is zero by definition.

Test Your Understanding

Why does memoization significantly improve the performance of the recursive LIS solution?

Because it caches the results of subproblems (LIS lengths at each index), preventing repeated computations of the same states and reducing the exponential recursion tree to a polynomial number of computations.

Related Problems

Dynamic Programming pattern

Don't just read it. Drill it.

Reconstruct Longest Increasing Subsequence from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Longest Increasing Subsequence drill

or drill a free problem