Facebook Pixel

2902. Count of Sub-Multisets With Bounded Sum

Problem Description

This problem asks you to count the number of sub-multisets from a given array nums where the sum of elements falls within the range [l, r].

A sub-multiset is a collection of elements from the array where:

  • Each element can appear 0 or more times (up to its frequency in the original array)
  • Order doesn't matter (it's unordered)
  • Two sub-multisets are considered the same if they contain the same elements with the same frequencies

For example, if nums = [1,2,2,3]:

  • You can use 0, 1, or 2 copies of the element 2 (since it appears twice)
  • You can use 0 or 1 copy of elements 1 and 3 (since they appear once each)
  • The empty set is valid and has a sum of 0

The solution uses dynamic programming with an optimization for handling multiple occurrences of the same element:

  1. Initial Setup: Create a DP array where dp[i] represents the count of sub-multisets with sum i. Handle zeros separately since they don't affect the sum but multiply the number of ways (you can include 0, 1, 2, ... up to all zeros).

  2. Processing Each Unique Element: For each unique number and its frequency:

    • Create a stride array that accumulates sums: stride[i] = dp[i] + dp[i-num] + dp[i-2*num] + ...
    • This represents all possible ways to form sum i using any number of copies of num
    • Then adjust for the frequency limit: if we can use at most freq copies, we subtract the contributions from using more than freq copies
  3. Final Count: Sum up all dp[i] for i in range [l, r] and multiply by (zeros + 1) to account for all possible ways to include zeros.

The modulo operation 10^9 + 7 is applied to keep the result within bounds since the count can be very large.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing this as a variant of the classic "subset sum" problem, but with a twist: elements can be used multiple times based on their frequency in the original array.

Let's think about how to build sub-multisets incrementally. If we have a DP solution for some elements, how do we incorporate a new element that appears freq times?

The naive approach would be to iterate through each possible count (0 to freq) of the new element and update our DP. For an element num appearing freq times, we'd need to consider adding 0*num, 1*num, 2*num, ..., freq*num to existing sums. This would be inefficient with nested loops.

The clever optimization comes from recognizing a pattern. When we want to know how many ways to form sum i using up to freq copies of num, we can think of it as:

  • Ways to form i without using num (which is dp[i])
  • Plus ways to form i-num (then add one num)
  • Plus ways to form i-2*num (then add two num)
  • And so on...

This forms a cumulative sum pattern! The stride array efficiently captures this: stride[i] contains the sum dp[i] + dp[i-num] + dp[i-2*num] + ..., representing all ways to form sum i using any number of copies of num.

But wait - we can only use up to freq copies. So if stride[i] includes contributions from using more than freq copies, we need to subtract those out. That's why we compute stride[i] - stride[i - num*(freq+1)] when possible - this removes the contribution from using freq+1 or more copies.

Zeros are handled separately because they're special: adding zeros doesn't change the sum, but each zero doubles our options (include it or not). So if we have zeros zeros, we multiply our final count by (zeros + 1) to account for all 2^zeros ways to include them.

This approach transforms an exponential problem into a polynomial one by cleverly reusing computations through the stride technique.

Learn more about Dynamic Programming and Sliding Window patterns.

Solution Approach

The implementation uses dynamic programming with a clever optimization for handling multiple occurrences of elements:

1. Initialization and Setup

dp = [1] + [0] * r
count = collections.Counter(nums)
zeros = count.pop(0, 0)
  • Create a DP array where dp[i] tracks the number of sub-multisets with sum exactly i
  • Initialize dp[0] = 1 (one way to make sum 0: empty set)
  • Count frequencies of each element using Counter
  • Extract and handle zeros separately since they don't affect sums

2. Processing Each Unique Element

For each unique number and its frequency:

for num, freq in count.items():
    stride = dp.copy()
    for i in range(num, r + 1):
        stride[i] += stride[i - num]
  • Create a stride array as a copy of current dp
  • Build cumulative sums: stride[i] = dp[i] + dp[i-num] + dp[i-2*num] + ...
  • This represents using 0, 1, 2, ... copies of num to form sum i

3. Applying Frequency Constraints

for i in range(r, 0, -1):
    if i >= num * (freq + 1):
        dp[i] = stride[i] - stride[i - num * (freq + 1)]
    else:
        dp[i] = stride[i]
  • Iterate backwards through sums to avoid interference
  • If i >= num * (freq + 1), we can use the difference formula:
    • stride[i] includes all ways using unlimited copies of num
    • stride[i - num * (freq + 1)] represents ways using at least freq + 1 copies
    • The difference gives us ways using at most freq copies
  • Otherwise, stride[i] already respects the frequency limit (can't use more than i/num copies anyway)

4. Computing Final Result

return (zeros + 1) * sum(dp[l : r + 1]) % kMod
  • Sum all valid sub-multiset counts in range [l, r]
  • Multiply by (zeros + 1) to account for all ways to include/exclude zeros
  • Apply modulo to keep result within bounds

Key Insights:

  • The stride technique transforms O(freq) operations per sum into O(1) by precomputing cumulative sums
  • Processing in reverse order prevents using updated values prematurely
  • Separating zeros simplifies logic since they're multiplicative factors rather than additive
  • Total time complexity: O(n * r) where n is the number of unique elements and r is the upper bound

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with nums = [1, 2, 2, 3], l = 1, r = 5.

Step 1: Initialize

  • Count frequencies: {1: 1, 2: 2, 3: 1} (no zeros)
  • Create DP array: dp = [1, 0, 0, 0, 0, 0] (index represents sum 0-5)
  • dp[0] = 1 means one way to make sum 0 (empty set)

Step 2: Process element 1 (frequency = 1)

  • Create stride array: stride = [1, 0, 0, 0, 0, 0]
  • Build cumulative sums for i = 1 to 5:
    • stride[1] = stride[1] + stride[0] = 0 + 1 = 1
    • stride[2] = stride[2] + stride[1] = 0 + 1 = 1
    • stride[3] = stride[3] + stride[2] = 0 + 1 = 1
    • stride[4] = stride[4] + stride[3] = 0 + 1 = 1
    • stride[5] = stride[5] + stride[4] = 0 + 1 = 1
  • Apply frequency constraint (freq = 1, so max 1 copy):
    • For i = 5: 5 >= 1*(1+1) = 2, so dp[5] = stride[5] - stride[3] = 1 - 1 = 0
    • For i = 4: 4 >= 2, so dp[4] = stride[4] - stride[2] = 1 - 1 = 0
    • For i = 3: 3 >= 2, so dp[3] = stride[3] - stride[1] = 1 - 1 = 0
    • For i = 2: 2 >= 2, so dp[2] = stride[2] - stride[0] = 1 - 1 = 0
    • For i = 1: 1 < 2, so dp[1] = stride[1] = 1
  • Result: dp = [1, 1, 0, 0, 0, 0] (can make sum 0 with {} and sum 1 with {1})

Step 3: Process element 2 (frequency = 2)

  • Create stride array: stride = [1, 1, 0, 0, 0, 0]
  • Build cumulative sums:
    • stride[2] = 0 + stride[0] = 0 + 1 = 1
    • stride[3] = 0 + stride[1] = 0 + 1 = 1
    • stride[4] = 0 + stride[2] = 0 + 1 = 1
    • stride[5] = 0 + stride[3] = 0 + 1 = 1
  • Apply frequency constraint (freq = 2, so max 2 copies):
    • For i = 5: 5 < 2*(2+1) = 6, so dp[5] = stride[5] = 1
    • For i = 4: 4 < 6, so dp[4] = stride[4] = 1
    • For i = 3: 3 < 6, so dp[3] = stride[3] = 1
    • For i = 2: 2 < 6, so dp[2] = stride[2] = 1
    • For i = 1: 1 < 6, so dp[1] = stride[1] = 1
  • Result: dp = [1, 1, 1, 1, 1, 1]
  • Possible sets: {}, {1}, {2}, {1,2}, {2,2}, {1,2,2}

Step 4: Process element 3 (frequency = 1)

  • Create stride array: stride = [1, 1, 1, 1, 1, 1]
  • Build cumulative sums:
    • stride[3] = 1 + stride[0] = 1 + 1 = 2
    • stride[4] = 1 + stride[1] = 1 + 1 = 2
    • stride[5] = 1 + stride[2] = 1 + 1 = 2
  • Apply frequency constraint (freq = 1, so max 1 copy):
    • For i = 5: 5 < 3*(1+1) = 6, so dp[5] = stride[5] = 2
    • For i = 4: 4 < 6, so dp[4] = stride[4] = 2
    • For i = 3: 3 < 6, so dp[3] = stride[3] = 2
    • For i = 2: 2 < 6, so dp[2] = stride[2] = 1
    • For i = 1: 1 < 6, so dp[1] = stride[1] = 1
  • Final: dp = [1, 1, 1, 2, 2, 2]

Step 5: Calculate result

  • Sum for range [1, 5]: dp[1] + dp[2] + dp[3] + dp[4] + dp[5] = 1 + 1 + 2 + 2 + 2 = 8
  • No zeros to multiply, so answer = 8

The 8 valid sub-multisets with sums in [1, 5] are:

  1. {1} → sum = 1
  2. {2} → sum = 2
  3. {3} → sum = 3
  4. {1,2} → sum = 3
  5. {2,2} → sum = 4
  6. {1,3} → sum = 4
  7. {2,3} → sum = 5
  8. {1,2,2} → sum = 5

Solution Implementation

1class Solution:
2    def countSubMultisets(self, nums: List[int], l: int, r: int) -> int:
3        MOD = 1_000_000_007
4      
5        # Initialize DP array where dp[i] represents the number of submultisets with sum i
6        # dp[0] = 1 because there's one way to make sum 0 (empty submultiset)
7        dp = [1] + [0] * r
8      
9        # Count frequency of each number in nums
10        frequency_map = collections.Counter(nums)
11      
12        # Handle zeros separately since they don't contribute to sum
13        # but increase the count of valid submultisets
14        zero_count = frequency_map.pop(0, 0)
15      
16        # Process each unique number and its frequency
17        for number, frequency in frequency_map.items():
18            # Create a prefix sum array to efficiently calculate
19            # dp[i] + dp[i - number] + dp[i - 2*number] + ...
20            prefix_sum = dp.copy()
21          
22            # Build prefix sum array by accumulating values
23            for sum_value in range(number, r + 1):
24                prefix_sum[sum_value] += prefix_sum[sum_value - number]
25          
26            # Update dp array considering we can use 0 to 'frequency' copies of 'number'
27            for sum_value in range(r, 0, -1):
28                if sum_value >= number * (frequency + 1):
29                    # If sum is large enough, we subtract the contribution of
30                    # using more than 'frequency' copies of the current number
31                    # This gives us exactly 0 to 'frequency' copies
32                    dp[sum_value] = prefix_sum[sum_value] - prefix_sum[sum_value - number * (frequency + 1)]
33                else:
34                    # If sum is small, we can use all available combinations
35                    dp[sum_value] = prefix_sum[sum_value]
36      
37        # Calculate final result:
38        # - Sum dp values from l to r (inclusive)
39        # - Multiply by (zero_count + 1) to account for all possible zero selections
40        # - Apply modulo to keep result within bounds
41        result = (zero_count + 1) * sum(dp[l:r + 1]) % MOD
42      
43        return result
44
1class Solution {
2    private static final int MOD = 1_000_000_007;
3  
4    public int countSubMultisets(List<Integer> nums, int l, int r) {
5        // Count frequency of each number
6        Map<Integer, Integer> frequencyMap = new HashMap<>();
7        int totalSum = 0;
8      
9        for (int num : nums) {
10            totalSum += num;
11            // Only consider numbers that don't exceed r (optimization)
12            if (num <= r) {
13                frequencyMap.merge(num, 1, Integer::sum);
14            }
15        }
16      
17        // If total sum is less than l, no valid submultiset exists
18        if (totalSum < l) {
19            return 0;
20        }
21      
22        // Optimize upper bound - can't exceed total sum
23        r = Math.min(r, totalSum);
24      
25        // dp[i] represents number of ways to form sum i
26        int[] dp = new int[r + 1];
27      
28        // Handle zeros separately - they don't contribute to sum but multiply possibilities
29        // If we have k zeros, we have (k+1) ways to include them (0, 1, 2, ..., k zeros)
30        dp[0] = frequencyMap.getOrDefault(0, 0) + 1;
31        frequencyMap.remove(Integer.valueOf(0));
32      
33        int currentMaxSum = 0;
34      
35        // Process each unique number and its frequency
36        for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
37            int number = entry.getKey();
38            int frequency = entry.getValue();
39          
40            // Update the maximum sum we can achieve so far
41            currentMaxSum = Math.min(currentMaxSum + frequency * number, r);
42          
43            // Phase 1: Add contributions
44            // For each sum i, add ways to form it using current number
45            // dp[i] += dp[i - number] + dp[i - 2*number] + ... + dp[i - frequency*number]
46            for (int targetSum = number; targetSum <= currentMaxSum; targetSum++) {
47                dp[targetSum] = (dp[targetSum] + dp[targetSum - number]) % MOD;
48            }
49          
50            // Phase 2: Remove overcounting
51            // We need to subtract cases where we use more than 'frequency' copies
52            // This corrects the rolling sum to only include valid frequencies
53            int overflowThreshold = (frequency + 1) * number;
54            for (int targetSum = currentMaxSum; targetSum >= overflowThreshold; targetSum--) {
55                dp[targetSum] = (dp[targetSum] - dp[targetSum - overflowThreshold] + MOD) % MOD;
56            }
57        }
58      
59        // Sum up all valid submultiset counts in range [l, r]
60        int result = 0;
61        for (int sum = l; sum <= r; sum++) {
62            result = (result + dp[sum]) % MOD;
63        }
64      
65        return result;
66    }
67}
68
1class Solution {
2public:
3    int countSubMultisets(const vector<int>& nums, int l, int r) {
4        // Constants
5        const int MAX_VALUE = 20001;
6        const int MOD = 1000000007;
7      
8        // frequency[i] stores the count of element i in nums
9        int frequency[MAX_VALUE] = {};
10      
11        // dp[i] stores the number of ways to form sum i
12        int dp[MAX_VALUE] = {};
13      
14        // Count frequency of each element
15        for (int num : nums) {
16            ++frequency[num];
17        }
18      
19        // Initialize: handle element 1 specially
20        // We can form sums 0, 1, 2, ..., frequency[1] using only 1s
21        fill_n(dp, frequency[1] + 1, 1);
22      
23        // Process each unique element from 2 to r
24        int currentTotal = frequency[1];  // Track maximum possible sum so far
25      
26        for (int value = 2; value <= r; ++value) {
27            // Skip if this value doesn't appear in nums
28            if (!frequency[value]) {
29                continue;
30            }
31          
32            // Maximum contribution from this value
33            int maxContribution = (frequency[value] + 1) * value;
34          
35            // Update the maximum possible sum
36            currentTotal += value * frequency[value];
37          
38            // Phase 1: Add contributions from using this value
39            // dp[i] += dp[i-value] means we can form sum i by adding value to sum (i-value)
40            for (int sum = value, maxSum = min(currentTotal, r); sum <= maxSum; ++sum) {
41                dp[sum] = (dp[sum] + dp[sum - value]) % MOD;
42            }
43          
44            // Phase 2: Remove overcounting when we use more than frequency[value] copies
45            // This implements the sliding window technique for bounded knapsack
46            for (int sum = min(currentTotal, r); sum >= maxContribution; --sum) {
47                dp[sum] = (MOD + dp[sum] - dp[sum - maxContribution]) % MOD;
48            }
49        }
50      
51        // Calculate the sum of dp values in range [l, r]
52        long long result = accumulate(dp + l, dp + r + 1, 0LL);
53      
54        // Multiply by (frequency[0] + 1) to account for including 0 to frequency[0] zeros
55        // in each submultiset (zeros don't affect the sum)
56        return result * (frequency[0] + 1) % MOD;
57    }
58};
59
1/**
2 * Counts the number of sub-multisets with sum between l and r (inclusive)
3 * @param nums - Array of integers to form sub-multisets from
4 * @param l - Minimum sum (inclusive)
5 * @param r - Maximum sum (inclusive)
6 * @returns Number of valid sub-multisets modulo 10^9 + 7
7 */
8function countSubMultisets(nums: number[], l: number, r: number): number {
9    // Count frequency of each number (0 to 20000)
10    const frequency: number[] = Array(20001).fill(0);
11    // Dynamic programming array to store number of ways to achieve each sum
12    const dp: number[] = Array(20001).fill(0);
13    // Modulo value for the result
14    const MOD: number = 1000000007;
15  
16    // Count occurrences of each number in nums
17    for (const num of nums) {
18        frequency[num]++;
19    }
20  
21    // Initialize dp array for handling zeros and ones
22    // Each occurrence of 1 can be included or excluded, plus the empty set
23    dp.fill(1, 0, frequency[1] + 1);
24  
25    // Track the maximum possible sum achieved so far
26    let maxSum: number = frequency[1];
27  
28    // Process each unique number from 2 to r
29    for (let num = 2; num <= r; ++num) {
30        // Skip if this number doesn't appear in nums
31        if (!frequency[num]) {
32            continue;
33        }
34      
35        // Maximum contribution from this number (all occurrences used)
36        const maxContribution: number = (frequency[num] + 1) * num;
37        // Update the maximum possible sum
38        maxSum += num * frequency[num];
39      
40        // Update dp array for sums that can include this number
41        // Forward pass: add ways to form sums by including this number
42        for (let sum = num, upperBound = Math.min(maxSum, r); sum <= upperBound; ++sum) {
43            dp[sum] = (dp[sum] + dp[sum - num]) % MOD;
44        }
45      
46        // Backward pass: remove overcounted ways (sliding window technique)
47        // This handles the constraint on maximum usage of the current number
48        for (let sum = Math.min(maxSum, r); sum >= maxContribution; --sum) {
49            dp[sum] = (MOD + dp[sum] - dp[sum - maxContribution]) % MOD;
50        }
51    }
52  
53    // Sum up all valid sub-multiset counts in the range [l, r]
54    let result: number = 0;
55    for (let sum = l; sum <= r; sum++) {
56        result = (result + dp[sum]) % MOD;
57    }
58  
59    // Multiply by (frequency[0] + 1) to account for including/excluding zeros
60    // Zeros don't affect the sum but create different sub-multisets
61    return (result * (frequency[0] + 1)) % MOD;
62}
63

Time and Space Complexity

Time Complexity: O(n * r) where n is the number of unique elements in nums and r is the upper bound of the sum range.

The algorithm iterates through each unique element in the counter (at most n unique elements). For each unique element with value num:

  • Creating the stride array takes O(r) time
  • Computing the prefix sums in stride takes O(r) time (iterating from num to r)
  • Updating the dp array takes O(r) time (iterating from r down to 1)

Therefore, for each unique element, we perform O(r) operations, resulting in a total time complexity of O(n * r).

Space Complexity: O(r)

The space complexity is dominated by:

  • The dp array of size r + 1: O(r)
  • The stride array of size r + 1: O(r)
  • The count dictionary storing at most n unique elements: O(n)

Since typically r is expected to be larger than n in problems involving sum ranges, and we're using O(r) space for the arrays, the overall space complexity is O(r).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Update Order in DP Array

One of the most critical pitfalls is updating the DP array in the wrong order when applying frequency constraints. If you update from low to high indices instead of high to low, you'll use already-modified values in your calculations, leading to incorrect results.

Wrong approach:

# This will cause errors - using updated values prematurely
for sum_value in range(1, r + 1):  # Forward iteration - WRONG!
    if sum_value >= number * (frequency + 1):
        dp[sum_value] = prefix_sum[sum_value] - prefix_sum[sum_value - number * (frequency + 1)]
    else:
        dp[sum_value] = prefix_sum[sum_value]

Correct approach:

# Process in reverse to avoid using modified values
for sum_value in range(r, 0, -1):  # Backward iteration - CORRECT!
    if sum_value >= number * (frequency + 1):
        dp[sum_value] = prefix_sum[sum_value] - prefix_sum[sum_value - number * (frequency + 1)]
    else:
        dp[sum_value] = prefix_sum[sum_value]

2. Forgetting to Handle Zero Elements Separately

Zeros require special handling because they don't contribute to the sum but multiply the number of valid sub-multisets. Forgetting to extract and handle zeros separately will give incorrect counts.

Wrong approach:

# Processing zeros like regular numbers - WRONG!
for number, frequency in frequency_map.items():
    # This will incorrectly handle zeros
    prefix_sum = dp.copy()
    for sum_value in range(number, r + 1):
        prefix_sum[sum_value] += prefix_sum[sum_value - number]

Correct approach:

# Extract zeros first and handle them separately
zero_count = frequency_map.pop(0, 0)  # Remove zeros from processing
# ... process non-zero elements ...
# Then multiply final result by (zero_count + 1)
result = (zero_count + 1) * sum(dp[l:r + 1]) % MOD

3. Modulo Operation Timing

Applying modulo operation at the wrong time or forgetting it entirely can cause integer overflow or incorrect results.

Wrong approach:

# Forgetting modulo during intermediate calculations - can cause overflow
for sum_value in range(number, r + 1):
    prefix_sum[sum_value] += prefix_sum[sum_value - number]  # No modulo - RISKY!

# Or applying modulo incorrectly at the end
result = (zero_count + 1) * sum(dp[l:r + 1])
return result % MOD  # Overflow might have already occurred

Correct approach:

# Apply modulo during intermediate steps if values get large
for sum_value in range(number, r + 1):
    prefix_sum[sum_value] = (prefix_sum[sum_value] + prefix_sum[sum_value - number]) % MOD

# And ensure final calculation doesn't overflow
result = ((zero_count + 1) % MOD * sum(dp[l:r + 1]) % MOD) % MOD

4. Off-by-One Errors in Range Calculations

The problem asks for sums in range [l, r] inclusive. Common mistakes include using exclusive ranges or incorrect array slicing.

Wrong approaches:

# Exclusive upper bound - WRONG!
return (zero_count + 1) * sum(dp[l:r]) % MOD  # Missing dp[r]

# Or incorrect range check
for sum_value in range(r + 1):  # Should start from r, not r+1 in reverse
    # ...

Correct approach:

# Include both l and r in the final sum
return (zero_count + 1) * sum(dp[l:r + 1]) % MOD  # r+1 for inclusive range

5. Not Creating a Copy of DP Array for Prefix Sum

Using the same array for both DP and prefix sum calculations will corrupt your data.

Wrong approach:

# Modifying dp directly - WRONG!
for sum_value in range(number, r + 1):
    dp[sum_value] += dp[sum_value - number]  # Corrupts original dp values

Correct approach:

# Create a separate copy for prefix sum calculations
prefix_sum = dp.copy()
for sum_value in range(number, r + 1):
    prefix_sum[sum_value] += prefix_sum[sum_value - number]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More