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
and3
(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:
-
Initial Setup: Create a DP array where
dp[i]
represents the count of sub-multisets with sumi
. 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). -
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 ofnum
- Then adjust for the frequency limit: if we can use at most
freq
copies, we subtract the contributions from using more thanfreq
copies
- Create a
-
Final Count: Sum up all
dp[i]
fori
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.
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 usingnum
(which isdp[i]
) - Plus ways to form
i-num
(then add onenum
) - Plus ways to form
i-2*num
(then add twonum
) - 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 exactlyi
- 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 currentdp
- 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 sumi
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 ofnum
stride[i - num * (freq + 1)]
represents ways using at leastfreq + 1
copies- The difference gives us ways using at most
freq
copies
- Otherwise,
stride[i]
already respects the frequency limit (can't use more thani/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 intoO(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)
wheren
is the number of unique elements andr
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 EvaluatorExample 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
, sodp[5] = stride[5] - stride[3] = 1 - 1 = 0
- For i = 4:
4 >= 2
, sodp[4] = stride[4] - stride[2] = 1 - 1 = 0
- For i = 3:
3 >= 2
, sodp[3] = stride[3] - stride[1] = 1 - 1 = 0
- For i = 2:
2 >= 2
, sodp[2] = stride[2] - stride[0] = 1 - 1 = 0
- For i = 1:
1 < 2
, sodp[1] = stride[1] = 1
- For i = 5:
- 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
, sodp[5] = stride[5] = 1
- For i = 4:
4 < 6
, sodp[4] = stride[4] = 1
- For i = 3:
3 < 6
, sodp[3] = stride[3] = 1
- For i = 2:
2 < 6
, sodp[2] = stride[2] = 1
- For i = 1:
1 < 6
, sodp[1] = stride[1] = 1
- For i = 5:
- 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
, sodp[5] = stride[5] = 2
- For i = 4:
4 < 6
, sodp[4] = stride[4] = 2
- For i = 3:
3 < 6
, sodp[3] = stride[3] = 2
- For i = 2:
2 < 6
, sodp[2] = stride[2] = 1
- For i = 1:
1 < 6
, sodp[1] = stride[1] = 1
- For i = 5:
- 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} → sum = 1
- {2} → sum = 2
- {3} → sum = 3
- {1,2} → sum = 3
- {2,2} → sum = 4
- {1,3} → sum = 4
- {2,3} → sum = 5
- {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 takesO(r)
time - Computing the prefix sums in
stride
takesO(r)
time (iterating fromnum
tor
) - Updating the
dp
array takesO(r)
time (iterating fromr
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 sizer + 1
:O(r)
- The
stride
array of sizer + 1
:O(r)
- The
count
dictionary storing at mostn
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]
Which of these properties could exist for a graph but not a tree?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!