Number of Longest Increasing Subsequence
MedPrompt
Given an integer array nums, return the number of longest increasing subsequences.
Notice that the sequence has to be strictly increasing.
Example 1:
Input: nums = [1,3,5,4,7]
Output: 2
Explanation: The two longest increasing subsequences are [1, 3, 4, 7] and [1, 3, 5, 7].Example 2:
Input: nums = [2,2,2,2,2]
Output: 5
Explanation: The length of the longest increasing subsequence is 1, and there are 5 increasing subsequences of length 1, so output 5.
Constraints:
1 <= nums.length <= 2000-106 <= nums[i] <= 106- The answer is guaranteed to fit inside a 32-bit integer.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses dynamic programming to solve the problem. It maintains two arrays: one to track the length of the longest increasing subsequence (LIS) ending at each position, and another to track the count of such subsequences. By iterating through the array and comparing each element with all previous elements, it builds up the lengths and counts for all possible LIS.
Algorithm
- Initialize two arrays,
lengthandcount, of sizen(the length ofnums), and fill them with1. length[i]will store the length of the longest increasing subsequence (LIS) ending at indexi.count[i]will store the number of such LIS ending at indexi.- Iterate through the
numsarray with an outer loop fromi = 0ton-1. - Inside, have a nested loop from
j = 0toi-1. - If
nums[i] > nums[j], we can extend the subsequence ending atj.- If
length[j] + 1 > length[i], we have found a new, longer LIS ending ati. Updatelength[i] = length[j] + 1and resetcount[i] = count[j]. - If
length[j] + 1 == length[i], we have found another way to form an LIS of the same length. We add the counts:count[i] += count[j].
- If
- After the loops, find the maximum length (
maxLength) in thelengtharray. - Iterate through the
lengtharray one last time. Sum up allcount[i]wherelength[i]is equal tomaxLength. - Return the final sum.
Walkthrough
In this method, we use two arrays, length and count, both of size n, where n is the length of the input array nums.
length[i]: Stores the length of the longest increasing subsequence that ends with the elementnums[i].count[i]: Stores the number of distinct longest increasing subsequences that end with the elementnums[i].
We initialize length[i] and count[i] to 1 for all i, as any single element itself is an increasing subsequence of length 1.
We then iterate from the first element to the last. For each element nums[i], we iterate through all previous elements nums[j] (where j < i). If nums[i] is strictly greater than nums[j], it means we can extend an increasing subsequence ending at j.
There are two cases:
- If extending the subsequence at
jgives a longer subsequence fori(i.e.,length[j] + 1 > length[i]), we've found a new maximum length for LIS ending ati. We updatelength[i]to this new length and setcount[i]to becount[j], because all the ways to form the LIS ending atjnow extend to a new LIS ending ati. - If extending the subsequence at
jresults in a subsequence of the same length as the current LIS fori(i.e.,length[j] + 1 == length[i]), it means we've found an alternative way to form the LIS of that length. So, we addcount[j]tocount[i].
After populating both arrays, we find the overall maxLength of any LIS by finding the maximum value in the length array. Finally, we sum up the count[i] for all indices i where length[i] equals maxLength to get the total number of longest increasing subsequences.
import java.util.Arrays; class Solution { public int findNumberOfLIS(int[] nums) { int n = nums.length; if (n <= 1) { return n; } // length[i] = length of LIS ending at nums[i] int[] length = new int[n]; // count[i] = number of LIS ending at nums[i] int[] count = new int[n]; Arrays.fill(length, 1); Arrays.fill(count, 1); for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { if (length[j] + 1 > length[i]) { length[i] = length[j] + 1; count[i] = count[j]; // Reset count } else if (length[j] + 1 == length[i]) { count[i] += count[j]; // Add ways } } } } int maxLength = 0; for (int len : length) { maxLength = Math.max(maxLength, len); } int result = 0; for (int i = 0; i < n; i++) { if (length[i] == maxLength) { result += count[i]; } } return result; }}Complexity
Time
O(N^2), where N is the length of `nums`. The nested loops to compare each element with all its predecessors result in a quadratic runtime.
Space
O(N), where N is the length of `nums`. We use two arrays, `length` and `count`, of size N.
Trade-offs
Pros
It is relatively straightforward to understand and implement.
The logic directly follows the definition of the problem.
Cons
The O(N^2) time complexity makes it slow for large input arrays, potentially leading to a 'Time Limit Exceeded' error on competitive programming platforms.
This advanced approach optimizes the O(N^2) DP solution to O(N log N) using a Segment Tree (or a Fenwick Tree) combined with coordinate compression. The core idea is to replace the linear scan for previous subsequences with an efficient logarithmic time query on a data structure. The Segment Tree stores information about the lengths and counts of LIS ending at various values, allowing for quick retrieval of the necessary data to extend a subsequence.
Algorithm
- Coordinate Compression: First, handle the potentially large range of numbers in
nums. Create a sorted list of unique values fromnumsand map each value to a rank (a small integer). This reduces the problem's value space to at mostN. - Segment Tree Setup: Initialize a Segment Tree over the range of ranks. Each node in the tree will store a pair
(length, count), representing the LIS length and the number of ways to achieve it within that node's range. Acombinefunction is needed to merge pairs from child nodes: if lengths differ, take the pair with the greater length; if lengths are equal, sum their counts. - Processing Elements: Iterate through each
numin the input arraynums.- Get the rank
rofnumfrom the map created earlier. - Query the Segment Tree for the range of ranks
[0, r-1]. This efficiently finds the(maxLength, count)pair for all LIS ending with a value smaller thannum. Let the result be(prevLen, prevCount). The base case for an empty range query is(0, 1)(for an empty subsequence). - The LIS ending with the current
numwill have lengthnewLen = prevLen + 1and countnewCount = prevCount. - Update the Segment Tree at rank
rwith this new pair(newLen, newCount), using thecombinelogic to merge with any existing data at that rank (for handling duplicate numbers).
- Get the rank
- Final Result: After processing all numbers, the final answer is the
countpart of the pair returned by querying the entire range of ranks in the Segment Tree.
Walkthrough
The O(N^2) DP approach is bottlenecked by the inner loop, which performs a linear scan to find the best previous subsequence to extend. We can optimize this search from O(N) to O(log N) using a data structure like a Segment Tree.
1. Coordinate Compression:
The values in nums can be large, but their exact values don't matter as much as their relative order. We can map each unique number in nums to a smaller integer rank (from 0 to m-1, where m is the number of unique elements). This step takes O(N log N) due to sorting.
2. Segment Tree:
We use a Segment Tree built over the range of these ranks. Each node in the tree will store a Pair object containing (length, count). This pair represents the maximum LIS length found within the node's range and the number of ways to achieve it.
We define a combine function to merge the results from two nodes. When combining (l1, c1) and (l2, c2):
- If
l1 > l2, the result is(l1, c1). - If
l2 > l1, the result is(l2, c2). - If
l1 == l2, the result is(l1, c1 + c2).
3. Algorithm Flow:
We iterate through each number num in the original nums array.
- For each
num, we find its rank,r. - We query our Segment Tree on the range
[0, r-1]. This query uses thecombinefunction to find the best(length, count)pair among all subsequences that could precedenum(i.e., those ending with a value smaller thannum). Let's say this query returns(prevLen, prevCount). A query on an empty range should logically return(0, 1)to represent the empty subsequence which has length 0 and 1 way to form. - The new LIS ending with
numwill have lengthnewLen = prevLen + 1and countnewCount = prevCount. - We then update the Segment Tree at the position
rwith this new(newLen, newCount)pair. The update also uses thecombinelogic to handle cases where multiple numbers have the same value.
After iterating through all numbers, the final answer is the count from the pair stored at the root of the Segment Tree, which represents the combined result over the entire range of ranks.
import java.util.*; class Solution { // A pair to store (length, count) class Pair { int length; int count; Pair(int length, int count) { this.length = length; this.count = count; } } // Segment Tree Node class Node { Pair pair; Node left, right; Node() { this.pair = new Pair(0, 0); this.left = null; this.right = null; } } // Combine function for two pairs private Pair combine(Pair p1, Pair p2) { if (p1.length > p2.length) return p1; if (p2.length > p1.length) return p2; return new Pair(p1.length, p1.count + p2.count); } // Update operation on the segment tree (implicit/dynamic) private void update(Node node, int start, int end, int index, Pair value) { if (start == end) { node.pair = combine(node.pair, value); return; } int mid = start + (end - start) / 2; if (index <= mid) { if (node.left == null) node.left = new Node(); update(node.left, start, mid, index, value); } else { if (node.right == null) node.right = new Node(); update(node.right, mid + 1, end, index, value); } Pair leftPair = (node.left != null) ? node.left.pair : new Pair(0, 0); Pair rightPair = (node.right != null) ? node.right.pair : new Pair(0, 0); node.pair = combine(leftPair, rightPair); } // Query operation on the segment tree private Pair query(Node node, int start, int end, int l, int r) { if (node == null || r < start || l > end || l > r) { return new Pair(0, 0); } if (l <= start && end <= r) { return node.pair; } int mid = start + (end - start) / 2; Pair leftResult = query(node.left, start, mid, l, r); Pair rightResult = query(node.right, mid + 1, end, l, r); return combine(leftResult, rightResult); } public int findNumberOfLIS(int[] nums) { if (nums == null || nums.length == 0) return 0; // Coordinate Compression Set<Integer> uniqueNums = new TreeSet<>(); for (int num : nums) { uniqueNums.add(num); } Map<Integer, Integer> rankMap = new HashMap<>(); int rank = 0; for (int num : uniqueNums) { rankMap.put(num, rank++); } int maxRank = rank - 1; Node root = new Node(); for (int num : nums) { int currentRank = rankMap.get(num); // Query for LIS ending with a value smaller than num Pair result = query(root, 0, maxRank, 0, currentRank - 1); // If no smaller element, LIS is of length 1, count 1 if (result.length == 0) { result = new Pair(0, 1); } int newLength = result.length + 1; int newCount = result.count; // Update the tree with the new LIS info for the current number update(root, 0, maxRank, currentRank, new Pair(newLength, newCount)); } return root.pair.count; }}Complexity
Time
O(N log N), where N is the length of `nums`. Coordinate compression takes O(N log N). The main loop runs N times, and each iteration involves a query and an update on the Segment Tree, both of which take O(log M) time, where M is the number of unique elements (M <= N).
Space
O(N), where N is the length of `nums`. The space is used for coordinate compression (map and set) and the Segment Tree. In the worst case (all unique elements), the tree can have O(N) nodes.
Trade-offs
Pros
Highly efficient with O(N log N) time complexity, making it suitable for large datasets.
Demonstrates a powerful technique combining data structures and dynamic programming.
Cons
The implementation is significantly more complex than the DP approach, requiring knowledge of Segment Trees (or Fenwick Trees) and coordinate compression.
The constant factors in the time complexity might be higher than the DP approach for very small N.
Solutions
Solution
class BinaryIndexedTree { private int n ; private int [] c ; private int [] d ; public BinaryIndexedTree ( int n ) { this . n = n ; c = new int [ n + 1 ]; d = new int [ n + 1 ]; } public void update ( int x , int v , int cnt ) { while ( x <= n ) { if ( c [ x ] < v ) { c [ x ] = v ; d [ x ] = cnt ; } else if ( c [ x ] == v ) { d [ x ] += cnt ; } x += x & - x ; } } public int [] query ( int x ) { int v = 0 , cnt = 0 ; while ( x > 0 ) { if ( c [ x ] > v ) { v = c [ x ]; cnt = d [ x ]; } else if ( c [ x ] == v ) { cnt += d [ x ]; } x -= x & - x ; } return new int [] { v , cnt }; } } public class Solution { public int findNumberOfLIS ( int [] nums ) { // int[] arr = Arrays.stream(nums).distinct().sorted().toArray(); int [] arr = nums . clone (); Arrays . sort ( arr ); int m = arr . length ; BinaryIndexedTree tree = new BinaryIndexedTree ( m ); for ( int x : nums ) { int i = Arrays . binarySearch ( arr , x ) + 1 ; int [] t = tree . query ( i - 1 ); int v = t [ 0 ]; int cnt = t [ 1 ]; tree . update ( i , v + 1 , Math . max ( cnt , 1 )); } return tree . query ( m )[ 1 ]; } }class BinaryIndexedTree { private: int n ; vector < int > c ; vector < int > d ; public: BinaryIndexedTree ( int n ) : n ( n ) , c ( n + 1 , 0 ) , d ( n + 1 , 0 ) {} void update ( int x , int v , int cnt ) { while ( x <= n ) { if ( c [ x ] < v ) { c [ x ] = v ; d [ x ] = cnt ; } else if ( c [ x ] == v ) { d [ x ] += cnt ; } x += x & - x ; } } pair < int , int > query ( int x ) { int v = 0 , cnt = 0 ; while ( x > 0 ) { if ( c [ x ] > v ) { v = c [ x ]; cnt = d [ x ]; } else if ( c [ x ] == v ) { cnt += d [ x ]; } x -= x & - x ; } return { v , cnt }; } }; class Solution { public: int findNumberOfLIS ( vector < int >& nums ) { vector < int > arr = nums ; sort ( arr . begin (), arr . end ()); arr . erase ( unique ( arr . begin (), arr . end ()), arr . end ()); int m = arr . size (); BinaryIndexedTree tree ( m ); for ( int x : nums ) { auto it = lower_bound ( arr . begin (), arr . end (), x ); int i = distance ( arr . begin (), it ) + 1 ; auto [ v , cnt ] = tree . query ( i - 1 ); tree . update ( i , v + 1 , max ( cnt , 1 )); } return tree . query ( m ). second ; } };class BinaryIndexedTree : __slots__ = [ "n" , "c" , "d" ] def __init__ ( self , n ): self . n = n self . c = [ 0 ] * ( n + 1 ) self . d = [ 0 ] * ( n + 1 ) def update ( self , x , v , cnt ): while x <= self . n : if self . c [ x ] < v : self . c [ x ] = v self . d [ x ] = cnt elif self . c [ x ] == v : self . d [ x ] += cnt x += x & - x def query ( self , x ): v = cnt = 0 while x : if self . c [ x ] > v : v = self . c [ x ] cnt = self . d [ x ] elif self . c [ x ] == v : cnt += self . d [ x ] x -= x & - x return v , cnt class Solution : def findNumberOfLIS ( self , nums : List [ int ]) -> int : arr = sorted ( set ( nums )) m = len ( arr ) tree = BinaryIndexedTree ( m ) for x in nums : i = bisect_left ( arr , x ) + 1 v , cnt = tree . query ( i - 1 ) tree . update ( i , v + 1 , max ( cnt , 1 )) return tree . query ( m )[ 1 ]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.