Skip to content

300. Longest Increasing Subsequence

from bisect import bisect_left


class Solution:
    """📈 Longest Increasing Subsequence.

    Problem:
    --------
    Given an integer array `nums`, return the length of the longest strictly increasing
    subsequence. A subsequence is obtained by deleting some or no elements without
    changing the order of the rest.

    Key Insight:
    ------------
    O(n^2) baselines:
    - `D[i] = 1 + max(D[j] for j < i if nums[j] < nums[i])`: the LIS ending at `i`.
    - LCS (LC 1143) of `nums` with `sorted(set(nums))`: a common subsequence is
      increasing (it follows the sorted copy) and strictly so (no duplicates there).

    Both waste work: for each length we only care about the **smallest possible tail**.
    A subsequence ending lower is at least as easy to extend later. So keep

        tails[k] = smallest tail of any increasing subsequence of length k + 1 so far

    `tails` is strictly increasing: a subsequence of length k + 2 has a length-(k + 1)
    prefix ending below its own tail, so `tails[k] < tails[k + 1]`. That makes it
    binary-searchable.

    Approach:
    ---------
    For each `x`, let `pos = bisect_left(tails, x)`, the first index with
    `tails[pos] >= x`:
    - `tails[pos - 1] < x`, so `x` extends a subsequence of length `pos` into one of
      length `pos + 1`. It cannot extend anything longer: those all end at `>= x`.
    - `pos == len(tails)`: `x` beats every tail and creates a new, longer length.
    - Otherwise `x <= tails[pos]`, so `x` is the new smallest tail for length `pos + 1`.
      No other entry changes.

    `bisect_left` keeps the increase strict: an `x` equal to a tail replaces it instead
    of extending it. `bisect_right` would compute the longest *non-decreasing*
    subsequence.

    Example (`nums = [10, 9, 2, 5, 3, 7, 101, 18]`, answer 4):

          x     pos   tails
          10    0     [10]
          9     0     [9]
          2     0     [2]
          5     1     [2, 5]
          3     1     [2, 3]
          7     2     [2, 3, 7]
          101   3     [2, 3, 7, 101]
          18    3     [2, 3, 7, 18]

    `tails` is *not* itself an LIS, only its length is meaningful: `[3, 4, 1]` ends with
    `tails = [1, 4]`. To recover an actual subsequence, store indices in `tails` and, for
    each `x`, a parent pointer to the index at `tails[pos - 1]`.

    Complexity:
    -----------
    - Time: O(n log n), one binary search per element
    - Space: O(n)
    """

    def lengthOfLIS(self, nums: list[int]) -> int:
        tails: list[int] = []
        for x in nums:
            pos = bisect_left(tails, x)
            if pos == len(tails):
                tails.append(x)
            else:
                tails[pos] = x
        return len(tails)