150NeetCode 150

109.Longest Increasing Subsequence

MediumBlind 751-D Dynamic Programming Β· 11/12

Problem

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

A subsequence keeps the original order but may skip elements: [3, 6, 2, 7] is a subsequence of [0, 3, 1, 6, 2, 2, 7]. "Strictly increasing" means each element is greater than the one before it β€” equal values do not count.

Examples

Input:  nums = [10, 9, 2, 5, 3, 7, 101, 18]
Output: 4
Explanation: [2, 3, 7, 101] (or [2, 5, 7, 18], etc.).
Input:  nums = [0, 1, 0, 3, 2, 3]
Output: 4
Explanation: [0, 1, 2, 3].
Input:  nums = [7, 7, 7, 7]
Output: 1
Explanation: Equal values are not strictly increasing.

Constraints

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4
  • Follow-up: can you do it in O(n log n)?

πŸ’‘ Key Insight

Let dp[i] = length of the longest increasing subsequence ending at index i. It extends the best earlier subsequence whose last value is smaller: dp[i] = 1 + max(dp[j] for j < i with nums[j] < nums[i]) (or 1 if none), and the answer is max(dp) β€” O(nΒ²). For O(n log n), keep tails[k] = the smallest possible tail value of any increasing subsequence of length k + 1. tails is always sorted, so each new number either extends the longest one or replaces the first tail >= it (found by binary search). The final size of tails is the answer.

Recursion (Brute Force)

TimeO(2ⁿ)SpaceO(n)

Intuition

Walk left to right, remembering the index prev of the last element we took. At index i, we can always skip nums[i]; we can take it only if it's larger than nums[prev]. Explore both choices.

Recurrence: f(i, prev) = max(f(i + 1, prev), 1 + f(i + 1, i) if prev == -1 or nums[i] > nums[prev]), with f(n, prev) = 0.

Algorithm

  1. dfs(i, prev): if i == n return 0.
  2. skip = dfs(i + 1, prev).
  3. If prev == -1 or nums[i] > nums[prev]: take = 1 + dfs(i + 1, i).
  4. Return max(skip, take); answer dfs(0, -1).

Visualization

nums = [1, 2, 4, 3]. The state (3, prev=1) β€” "at index 3, last taken was index 1" β€” is reached by two different paths.

flowchart TD
    A["f(0, -)"] -->|"take 1"| B["f(1, 0)"]
    A -->|"skip"| C["f(1, -)"]
    B -->|"take 2"| D["f(2, 1)"]
    B -->|"skip"| E["f(2, 0)"]
    D -->|"take 4"| F["f(3, 2)"]
    D -->|"skip"| G["f(3, 1)"]
    C -->|"take 2"| H["f(2, 1)"]
    H -->|"skip"| I["f(3, 1)"]
    G -->|"take 3"| J["f(4, 3) = 0"]

f(2, 1) and f(3, 1) each appear twice β€” the subtree below is recomputed.

Code

java
class Solution {
    public int lengthOfLIS(int[] nums) {
        return dfs(nums, 0, -1);
    }

    // longest increasing subsequence in nums[i..] given the last taken index prev
    private int dfs(int[] nums, int i, int prev) {
        if (i == nums.length) {
            return 0;
        }
        int skip = dfs(nums, i + 1, prev);
        int take = 0;
        if (prev == -1 || nums[i] > nums[prev]) {
            take = 1 + dfs(nums, i + 1, i);
        }
        return Math.max(skip, take);
    }
}

Complexity

  • Time: O(2ⁿ) β€” each element is taken or skipped.
  • Space: O(n) β€” recursion depth.

Complexity Comparison

Approach Time Space
Recursion (Brute Force) O(2ⁿ) O(n)
Top-Down Memoization O(nΒ²) O(nΒ²)
Bottom-Up Tabulation O(nΒ²) O(n)
Patience Sorting + Binary Search O(n log n) O(n)

Common Pitfalls

  • Returning dp[n - 1] instead of max(dp) β€” the LIS need not end at the last element.
  • Using <= in the comparison (or an upper-bound binary search), which counts equal values and breaks "strictly increasing" β€” [7, 7, 7] must give 1.
  • Assuming tails holds an actual LIS; it only has the right length. Reconstructing the sequence needs parent pointers.
  • Arrays.binarySearch returns an arbitrary match among duplicates and -(insertion point) - 1 otherwise; decode it carefully or write your own lower bound.