673. Number of Longest Increasing Subsequence - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Dynamic Programming - Both top-down (memoization) and bottom-up approaches for counting subproblems
  • Longest Increasing Subsequence (LIS) - Understanding the classic LIS problem and its O(n^2) DP solution
  • Recursion with Backtracking - Exploring all possible subsequences by making choices at each position
  • Binary Search - Used in the optimized O(n log n) solution for efficient lookups
  • Prefix Sums - Accumulating counts to efficiently query ranges in the optimized approach

1. Recursion

Intuition

The brute force approach explores every possible increasing subsequence by trying each element as a potential starting point. From each position, we recursively extend the subsequence by considering all future elements that are strictly greater than the current one. As we explore, we track the longest length found so far and count how many subsequences achieve that length. If we find a longer subsequence, we reset the count. If we find another subsequence of the same maximum length, we increment the count.

Algorithm

  1. Initialize LIS to track the maximum length found and res to count subsequences of that length.
  2. For each index i, start a dfs that treats element i as the beginning of a subsequence.
  3. In the dfs:
    • If the current length exceeds LIS, update LIS and reset res to 1.
    • If the current length equals LIS, increment res.
    • Try extending the subsequence with any element j > i where nums[j] > nums[i].
  4. Return res after exploring all starting positions.
class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        LIS = 0
        res = 0

        def dfs(i, length):
            nonlocal LIS, res
            if LIS < length:
                LIS = length
                res = 1
            elif LIS == length:
                res += 1

            for j in range(i + 1, len(nums)):
                if nums[j] <= nums[i]:
                    continue
                dfs(j, length + 1)

        for i in range(len(nums)):
            dfs(i, 1)
        return res

Time & Space Complexity

  • Time complexity: O(nn)O(n ^ n)
  • Space complexity: O(n)O(n)

2. Dynamic Programming (Top-Down)

Intuition

The recursive solution has overlapping subproblems since we repeatedly compute the longest increasing subsequence starting from the same index. By using memoization, we can store the result for each starting position after computing it once. For each index, we store both the length of the longest increasing subsequence starting there and the count of such subsequences. When we revisit an index, we simply return the cached result instead of recomputing.

Algorithm

  1. Create a memoization structure dp to store (maxLen, maxCnt) for each index.
  2. For each index i, call dfs(i) which:
    • Returns immediately if the result is already cached.
    • Initializes maxLen = 1 and maxCnt = 1 (the element itself forms a subsequence of length 1).
    • For each j > i where nums[j] > nums[i], recursively compute the result for j.
    • If 1 + length[j] exceeds maxLen, update maxLen and set maxCnt to count[j].
    • If 1 + length[j] equals maxLen, add count[j] to maxCnt.
    • Cache and return (maxLen, maxCnt).
  3. Aggregate results from all starting positions and return the count for the global maximum length.
class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        dp = {}

        def dfs(i):
            if i in dp:
                return

            maxLen = maxCnt = 1
            for j in range(i + 1, len(nums)):
                if nums[j] > nums[i]:
                    dfs(j)
                    length, count = dp[j]
                    if 1 + length > maxLen:
                        maxLen = length + 1
                        maxCnt = count
                    elif 1 + length == maxLen:
                        maxCnt += count
            dp[i] = (maxLen, maxCnt)

        lenLIS = res = 0
        for i in range(len(nums)):
            dfs(i)
            maxLen, maxCnt = dp[i]
            if maxLen > lenLIS:
                lenLIS = maxLen
                res = maxCnt
            elif maxLen == lenLIS:
                res += maxCnt

        return res

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(n)O(n)

3. Dynamic Programming (Bottom-Up)

Intuition

Instead of using recursion with memoization, we can iterate through the array in reverse order and build up the solution. For each position, we look at all positions to its right and determine the longest increasing subsequence that can be formed starting from the current position. This approach naturally ensures that when we process position i, all positions j > i have already been computed, giving us the information we need.

Algorithm

  1. Create a DP array where dp[i] = [maxLen, maxCnt] represents the longest increasing subsequence starting at index i and the count of such subsequences.
  2. Iterate from right to left (from n-1 to 0).
  3. For each index i:
    • Initialize maxLen = 1 and maxCnt = 1.
    • For each j > i where nums[j] > nums[i]:
      • If dp[j][0] + 1 > maxLen, update maxLen and set maxCnt = dp[j][1].
      • If dp[j][0] + 1 == maxLen, add dp[j][1] to maxCnt.
    • Store dp[i] = [maxLen, maxCnt].
    • Update the global maximum length and result count accordingly.
  4. Return the count of subsequences with the maximum length.
class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [[0, 0] for _ in range(n)]
        lenLIS, res = 0, 0

        for i in range(n - 1, -1, -1):
            maxLen, maxCnt = 1, 1
            for j in range(i + 1, n):
                if nums[j] > nums[i]:
                    length, count = dp[j]
                    if length + 1 > maxLen:
                        maxLen, maxCnt = length + 1, count
                    elif length + 1 == maxLen:
                        maxCnt += count

            if maxLen > lenLIS:
                lenLIS, res = maxLen, maxCnt
            elif maxLen == lenLIS:
                res += maxCnt
            dp[i] = [maxLen, maxCnt]

        return res

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(n)O(n)

4. Dynamic Programming (Binary Search + Prefix Sum)

Intuition

The O(n^2) DP solution can be optimized by using binary search and prefix sums. The key insight is to organize elements by the length of the longest increasing subsequence ending at them. For each length, we maintain a list of elements sorted in decreasing order along with cumulative counts. When processing a new element, we use binary search to find where it fits and to count how many subsequences of the previous length can be extended by this element.

Algorithm

  1. Initialize dp as a list of lists, where dp[len] contains pairs (value, cumulative_count) representing elements that end an increasing subsequence of length len+1.
  2. For each element in the array:
    • Use binary search (bs1) to find the longest subsequence length this element can extend.
    • Use binary search (bs2) on the previous length's list to count how many subsequences can be extended (those with values less than the current element).
    • If this element extends the maximum length, create a new entry in dp.
    • Otherwise, append the element to the appropriate length bucket with updated cumulative count.
  3. The answer is the cumulative count in the last entry of the longest length bucket.
class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [[[0, 0], [nums[0], 1]]]

        def bs1(num):
            l, r = 0, len(dp) - 1
            j = len(dp) - 1
            while l <= r:
                mid = (l + r) // 2
                if dp[mid][-1][0] < num:
                    l = mid + 1
                else:
                    j = mid
                    r = mid - 1
            return j

        def bs2(i, num):
            if i < 0:
                return 1
            l, r = 1, len(dp[i]) - 1
            j = 0
            while l <= r:
                mid = (l + r) // 2
                if dp[i][mid][0] >= num:
                    j = mid
                    l = mid + 1
                else:
                    r = mid - 1
            return dp[i][-1][1] - dp[i][j][1]

        LIS = 1
        for i in range(1, n):
            num = nums[i]
            if num > dp[-1][-1][0]:
                count = bs2(LIS - 1, num)
                dp.append([[0, 0], [num, count]])
                LIS += 1
            else:
                j = bs1(num)
                count = bs2(j - 1, num)
                dp[j].append([num, dp[j][-1][1] + count])

        return dp[-1][-1][1]

Time & Space Complexity

  • Time complexity: O(nlogn)O(n\log n)
  • Space complexity: O(n)O(n)

Common Pitfalls

Only Counting LIS Length Without Tracking Count

This problem requires counting how many subsequences achieve the maximum length, not just finding the length itself. A common mistake is to reuse standard LIS code that only tracks length. You must maintain both the length and the count of subsequences reaching that length at each position.

Resetting Count Instead of Accumulating

When finding a subsequence of the same maximum length, you should add to the count, not reset it. Only reset the count when you find a strictly longer subsequence. Confusing these cases leads to undercounting valid subsequences.

Using Non-Strict Inequality for Increasing Check

The problem asks for strictly increasing subsequences. Using nums[j] >= nums[i] instead of nums[j] > nums[i] will incorrectly include equal elements, leading to wrong answers on inputs with duplicate values.