673. Number of Longest Increasing Subsequence - Explanation
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
- Initialize
LISto track the maximum length found andresto count subsequences of that length. - For each index
i, start adfsthat treats elementias the beginning of a subsequence. - In the
dfs:- If the current length exceeds
LIS, updateLISand resetresto1. - If the current length equals
LIS, incrementres. - Try extending the subsequence with any element
j > iwherenums[j] > nums[i].
- If the current length exceeds
- Return
resafter 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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
- Create a memoization structure
dpto store(maxLen, maxCnt)for each index. - For each index
i, calldfs(i)which:- Returns immediately if the result is already cached.
- Initializes
maxLen = 1andmaxCnt = 1(the element itself forms a subsequence of length1). - For each
j > iwherenums[j] > nums[i], recursively compute the result forj. - If
1 + length[j]exceedsmaxLen, updatemaxLenand setmaxCnttocount[j]. - If
1 + length[j]equalsmaxLen, addcount[j]tomaxCnt. - Cache and return
(maxLen, maxCnt).
- 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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
- Create a DP array where
dp[i] = [maxLen, maxCnt]represents the longest increasing subsequence starting at indexiand the count of such subsequences. - Iterate from right to left (from
n-1to0). - For each index
i:- Initialize
maxLen = 1andmaxCnt = 1. - For each
j > iwherenums[j] > nums[i]:- If
dp[j][0] + 1 > maxLen, updatemaxLenand setmaxCnt = dp[j][1]. - If
dp[j][0] + 1 == maxLen, adddp[j][1]tomaxCnt.
- If
- Store
dp[i] = [maxLen, maxCnt]. - Update the global maximum length and result count accordingly.
- Initialize
- 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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
- Initialize
dpas a list of lists, wheredp[len]contains pairs(value, cumulative_count)representing elements that end an increasing subsequence of lengthlen+1. - 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.
- Use binary search (
- 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:
- Space complexity:
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.