3351. Sum of Good Subsequences
Problem Description
You are given an integer array nums. Your task is to find all possible good subsequences and return their sum.
A good subsequence is defined as a subsequence where the absolute difference between any two consecutive elements is exactly 1. In other words, if you pick elements from the array to form a subsequence, each adjacent pair in your subsequence must differ by exactly 1.
For example:
- If nums = [1, 2, 1], the good subsequences would be:[1],[2],[1](the individual elements),[1,2],[2,1], and[1,2,1]
- The subsequence [1,2]is good because|2-1| = 1
- The subsequence [1,1]would NOT be good because|1-1| = 0
Important points:
- A subsequence maintains the relative order of elements from the original array (you can skip elements but cannot rearrange them)
- Single elements are considered good subsequences by definition
- You need to return the sum of all elements across all possible good subsequences
- Since the answer can be very large, return it modulo 10^9 + 7
The solution uses dynamic programming with two tracking dictionaries:
- f[x]: tracks the sum of all good subsequences ending at value- x
- g[x]: tracks the count of all good subsequences ending at value- x
For each element in the array, the algorithm updates these values by considering:
- The element itself as a new subsequence
- Extending subsequences ending at x-1(one less than current)
- Extending subsequences ending at x+1(one more than current)
This approach efficiently calculates the total sum of all good subsequences in a single pass through the array.
Intuition
The key insight is that we need to track good subsequences as we build them, but we don't need to store the actual subsequences - just their sums and counts.
Think about what happens when we encounter a value x in the array. This value can:
- Start a new good subsequence by itself
- Extend any good subsequence that ends with x-1(since|x - (x-1)| = 1)
- Extend any good subsequence that ends with x+1(since|x - (x+1)| = 1)
This leads us to a dynamic programming approach where we maintain information about subsequences ending at each value.
Why do we need two tracking variables?
- Count (g[x]): We need to know how many good subsequences end with valuexbecause when we see anotherxlater, we can extend all of them
- Sum (f[x]): We need the sum of all good subsequences ending withxto build our final answer
When we process a new element x:
- We add xitself as a new subsequence (count increases by 1, sum increases byx)
- For all subsequences ending at x-1, we can appendxto create new subsequences. If there areg[x-1]such subsequences, we're addingxto each of them, contributingg[x-1] * xto the sum
- Similarly for subsequences ending at x+1, we addg[x+1] * xto the sum
The beauty of this approach is that we process each element once and update our tracking dictionaries on the fly. We don't need to generate all subsequences explicitly - we just keep track of the cumulative sums and counts grouped by the ending value.
At the end, the sum of all values in f gives us the total sum of all good subsequences, because f[v] contains the sum of all good subsequences ending with value v, and every good subsequence must end with some value.
Learn more about Dynamic Programming patterns.
Solution Approach
The implementation uses dynamic programming with two hash maps (dictionaries) to efficiently compute the sum of all good subsequences.
Data Structures:
- f: A dictionary where- f[x]stores the sum of all good subsequences ending with value- x
- g: A dictionary where- g[x]stores the count of all good subsequences ending with value- x
Algorithm Steps:
- 
Initialize empty dictionaries fandgusingdefaultdict(int)to handle missing keys automatically.
- 
Process each element xinnums:a. Add xas a new subsequence:- f[x] += x(add the value itself to the sum)
- g[x] += 1(increment count by 1)
 b. Extend subsequences ending at x-1:- f[x] += f[x - 1] + g[x - 1] * x- f[x - 1]: sum of all subsequences ending at- x-1
- g[x - 1] * x: contribution of appending- xto each of those subsequences
 
- g[x] += g[x - 1](add the count of subsequences we're extending)
 c. Extend subsequences ending at x+1:- f[x] += f[x + 1] + g[x + 1] * x- f[x + 1]: sum of all subsequences ending at- x+1
- g[x + 1] * x: contribution of appending- xto each of those subsequences
 
- g[x] += g[x + 1](add the count of subsequences we're extending)
 
- 
Calculate final result: - Sum all values in fdictionary:sum(f.values())
- Apply modulo 10^9 + 7to get the final answer
 
- Sum all values in 
Why this works:
- When we process an element x, we're considering all ways it can participate in good subsequences
- By updating both sum and count simultaneously, we maintain complete information about subsequences ending at each value
- The order of processing ensures that when we extend subsequences from x-1orx+1, we're using the most up-to-date information
- Each element can extend multiple existing subsequences, which is why we multiply by the count when adding to the sum
Time Complexity: O(n) where n is the length of the input array
Space Complexity: O(k) where k is the number of unique values in the array
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 the algorithm with nums = [2, 3, 1].
We'll track two dictionaries:
- f[x]: sum of all good subsequences ending with value- x
- g[x]: count of all good subsequences ending with value- x
Initial state:
- f = {},- g = {}
Processing element 2:
- Start new subsequence with just 2:- f[2] = 0 + 2 = 2(sum increases by 2)
- g[2] = 0 + 1 = 1(count increases by 1)
 
- Check for subsequences ending at 2-1=1: None exist
- Check for subsequences ending at 2+1=3: None exist
- State: f = {2: 2},g = {2: 1}
- Good subsequences so far: [2]
Processing element 3:
- Start new subsequence with just 3:- f[3] = 0 + 3 = 3
- g[3] = 0 + 1 = 1
 
- Extend subsequences ending at 3-1=2:- Found 1 subsequence ending at 2 with sum 2
- f[3] = 3 + 2 + (1 × 3) = 8
- g[3] = 1 + 1 = 2
 
- Check for subsequences ending at 3+1=4: None exist
- State: f = {2: 2, 3: 8},g = {2: 1, 3: 2}
- Good subsequences so far: [2],[3],[2,3]
Processing element 1:
- Start new subsequence with just 1:- f[1] = 0 + 1 = 1
- g[1] = 0 + 1 = 1
 
- Check for subsequences ending at 1-1=0: None exist
- Extend subsequences ending at 1+1=2:- Found 1 subsequence ending at 2 with sum 2
- f[1] = 1 + 2 + (1 × 1) = 4
- g[1] = 1 + 1 = 2
 
- State: f = {2: 2, 3: 8, 1: 4},g = {2: 1, 3: 2, 1: 2}
- Good subsequences so far: [2],[3],[2,3],[1],[2,1]
Final calculation:
- Sum of all values in f:2 + 8 + 4 = 14
Let's verify our subsequences and their sums:
- [2]→ sum = 2
- [3]→ sum = 3
- [2,3]→ sum = 5
- [1]→ sum = 1
- [2,1]→ sum = 3
- Total: 2 + 3 + 5 + 1 + 3 = 14 ✓
The algorithm correctly computes the sum of all good subsequences by tracking sums and counts of subsequences ending at each value, building up the solution incrementally.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5    def sumOfGoodSubsequences(self, nums: List[int]) -> int:
6        MOD = 10**9 + 7
7      
8        # sum_ending_at[val] = sum of all good subsequences ending with value val
9        sum_ending_at = defaultdict(int)
10      
11        # count_ending_at[val] = count of all good subsequences ending with value val
12        count_ending_at = defaultdict(int)
13      
14        for current_value in nums:
15            # Start a new subsequence with just the current value
16            sum_ending_at[current_value] += current_value
17            count_ending_at[current_value] += 1
18          
19            # Extend subsequences ending at (current_value - 1)
20            # Each such subsequence gets current_value added to it
21            sum_ending_at[current_value] += sum_ending_at[current_value - 1] + count_ending_at[current_value - 1] * current_value
22            count_ending_at[current_value] += count_ending_at[current_value - 1]
23          
24            # Extend subsequences ending at (current_value + 1)
25            # Each such subsequence gets current_value added to it
26            sum_ending_at[current_value] += sum_ending_at[current_value + 1] + count_ending_at[current_value + 1] * current_value
27            count_ending_at[current_value] += count_ending_at[current_value + 1]
28      
29        # Return the total sum of all good subsequences
30        return sum(sum_ending_at.values()) % MOD
311class Solution {
2    public int sumOfGoodSubsequences(int[] nums) {
3        final int MOD = (int) 1e9 + 7;
4      
5        // Find the maximum value in the array to determine array sizes
6        int maxValue = 0;
7        for (int num : nums) {
8            maxValue = Math.max(maxValue, num);
9        }
10      
11        // sumEndingAt[i]: sum of all good subsequences ending with value i
12        long[] sumEndingAt = new long[maxValue + 1];
13        // countEndingAt[i]: count of all good subsequences ending with value i
14        long[] countEndingAt = new long[maxValue + 1];
15      
16        // Process each number in the array
17        for (int currentNum : nums) {
18            // Store previous values to avoid interference during updates
19            long prevSum = sumEndingAt[currentNum];
20            long prevCount = countEndingAt[currentNum];
21          
22            // Start a new subsequence with just the current number
23            sumEndingAt[currentNum] = (prevSum + currentNum) % MOD;
24            countEndingAt[currentNum] = (prevCount + 1) % MOD;
25          
26            // Extend subsequences ending with (currentNum - 1)
27            if (currentNum > 0) {
28                // Add current number to all subsequences ending with (currentNum - 1)
29                long extendedSum = (sumEndingAt[currentNum - 1] + 
30                                   (long) countEndingAt[currentNum - 1] * currentNum % MOD) % MOD;
31                sumEndingAt[currentNum] = (sumEndingAt[currentNum] + extendedSum) % MOD;
32                countEndingAt[currentNum] = (countEndingAt[currentNum] + countEndingAt[currentNum - 1]) % MOD;
33            }
34          
35            // Extend subsequences ending with (currentNum + 1)
36            if (currentNum + 1 <= maxValue) {
37                // Add current number to all subsequences ending with (currentNum + 1)
38                long extendedSum = (sumEndingAt[currentNum + 1] + 
39                                   (long) countEndingAt[currentNum + 1] * currentNum % MOD) % MOD;
40                sumEndingAt[currentNum] = (sumEndingAt[currentNum] + extendedSum) % MOD;
41                countEndingAt[currentNum] = (countEndingAt[currentNum] + countEndingAt[currentNum + 1]) % MOD;
42            }
43        }
44      
45        // Calculate the total sum of all good subsequences
46        long totalSum = 0;
47        for (long sum : sumEndingAt) {
48            totalSum = (totalSum + sum) % MOD;
49        }
50      
51        return (int) totalSum;
52    }
53}
541class Solution {
2public:
3    int sumOfGoodSubsequences(vector<int>& nums) {
4        const int MOD = 1e9 + 7;
5      
6        // Find the maximum value in nums to determine array sizes
7        int maxValue = ranges::max(nums);
8      
9        // sum[i]: sum of all good subsequences ending with value i
10        // count[i]: number of good subsequences ending with value i
11        vector<long long> sum(maxValue + 1, 0);
12        vector<long long> count(maxValue + 1, 0);
13      
14        // Process each number in the array
15        for (int currentNum : nums) {
16            // Start a new subsequence with just the current number
17            sum[currentNum] += currentNum;
18            count[currentNum] += 1;
19          
20            // Extend subsequences ending with (currentNum - 1)
21            if (currentNum > 0) {
22                // Add sum of subsequences ending at currentNum-1 plus currentNum times their count
23                sum[currentNum] = (sum[currentNum] + sum[currentNum - 1] + 
24                                  (long long)count[currentNum - 1] * currentNum % MOD) % MOD;
25                // Add count of subsequences that can be extended
26                count[currentNum] = (count[currentNum] + count[currentNum - 1]) % MOD;
27            }
28          
29            // Extend subsequences ending with (currentNum + 1)
30            if (currentNum + 1 <= maxValue) {
31                // Add sum of subsequences ending at currentNum+1 plus currentNum times their count
32                sum[currentNum] = (sum[currentNum] + sum[currentNum + 1] + 
33                                  (long long)count[currentNum + 1] * currentNum % MOD) % MOD;
34                // Add count of subsequences that can be extended
35                count[currentNum] = (count[currentNum] + count[currentNum + 1]) % MOD;
36            }
37        }
38      
39        // Return the total sum of all good subsequences
40        return accumulate(sum.begin(), sum.end(), 0LL) % MOD;
41    }
42};
431function sumOfGoodSubsequences(nums: number[]): number {
2    const MOD = 10 ** 9 + 7;
3    const maxValue = Math.max(...nums);
4  
5    // sumArray[i] stores the sum of all good subsequences ending with value i
6    const sumArray: number[] = Array(maxValue + 1).fill(0);
7  
8    // countArray[i] stores the count of all good subsequences ending with value i
9    const countArray: number[] = Array(maxValue + 1).fill(0);
10  
11    for (const currentNum of nums) {
12        // Initialize: each number itself forms a subsequence
13        sumArray[currentNum] += currentNum;
14        countArray[currentNum] += 1;
15      
16        // Extend subsequences ending with (currentNum - 1)
17        if (currentNum > 0) {
18            // Add sum of subsequences ending with (currentNum - 1) plus currentNum times their count
19            sumArray[currentNum] = (sumArray[currentNum] + sumArray[currentNum - 1] + 
20                                   ((countArray[currentNum - 1] * currentNum) % MOD)) % MOD;
21            // Add count of subsequences ending with (currentNum - 1)
22            countArray[currentNum] = (countArray[currentNum] + countArray[currentNum - 1]) % MOD;
23        }
24      
25        // Extend subsequences ending with (currentNum + 1)
26        if (currentNum + 1 <= maxValue) {
27            // Add sum of subsequences ending with (currentNum + 1) plus currentNum times their count
28            sumArray[currentNum] = (sumArray[currentNum] + sumArray[currentNum + 1] + 
29                                   ((countArray[currentNum + 1] * currentNum) % MOD)) % MOD;
30            // Add count of subsequences ending with (currentNum + 1)
31            countArray[currentNum] = (countArray[currentNum] + countArray[currentNum + 1]) % MOD;
32        }
33    }
34  
35    // Return the total sum of all good subsequences
36    return sumArray.reduce((accumulator, currentValue) => (accumulator + currentValue) % MOD, 0);
37}
38Time and Space Complexity
Time Complexity: O(n), where n is the length of the input array nums.
The algorithm iterates through the array exactly once. For each element, it performs a constant number of operations:
- Dictionary lookups and updates for keys x,x-1, andx+1in bothfandgdictionaries
- Basic arithmetic operations (addition and multiplication)
- Each dictionary operation takes O(1)average time
After the main loop, sum(f.values()) takes O(k) time where k is the number of unique values in nums, and since k ≤ n, this is also O(n).
Therefore, the overall time complexity is O(n).
Space Complexity: O(k), where k is the number of unique values in the input array nums.
The algorithm uses two dictionaries (f and g):
- Each dictionary stores at most one entry for each unique value that appears in numsor is adjacent to a value innums(i.e.,x-1orx+1for somexinnums)
- In the worst case, this creates at most 3kentries across both dictionaries (for each unique valuex, we might have entries forx-1,x, andx+1)
- Since 3kis stillO(k), the space complexity isO(k)
In the worst case where all elements are unique, k = n, making the space complexity O(n).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Applying Modulo Operations During Computation
Problem: The code currently only applies modulo at the final return statement. When dealing with large arrays or large values, intermediate calculations can cause integer overflow, leading to incorrect results even before the final modulo operation.
Example scenario: If nums contains many elements or large values, the intermediate sums in sum_ending_at can exceed Python's integer limits in other languages, or cause performance issues with very large numbers.
Solution: Apply modulo operations after each addition to keep numbers manageable:
def sumOfGoodSubsequences(self, nums: List[int]) -> int:
    MOD = 10**9 + 7
    sum_ending_at = defaultdict(int)
    count_ending_at = defaultdict(int)
  
    for current_value in nums:
        # Calculate new sum and count for current_value
        new_sum = current_value
        new_count = 1
      
        # Extend from current_value - 1
        new_sum = (new_sum + sum_ending_at[current_value - 1] + 
                   count_ending_at[current_value - 1] * current_value) % MOD
        new_count = (new_count + count_ending_at[current_value - 1]) % MOD
      
        # Extend from current_value + 1
        new_sum = (new_sum + sum_ending_at[current_value + 1] + 
                   count_ending_at[current_value + 1] * current_value) % MOD
        new_count = (new_count + count_ending_at[current_value + 1]) % MOD
      
        # Update dictionaries
        sum_ending_at[current_value] = (sum_ending_at[current_value] + new_sum) % MOD
        count_ending_at[current_value] = (count_ending_at[current_value] + new_count) % MOD
  
    return sum(sum_ending_at.values()) % MOD
Pitfall 2: Incorrect Update Order Within Loop
Problem: The original code updates sum_ending_at[current_value] and count_ending_at[current_value] incrementally within the loop. This can lead to using partially updated values when the same value appears multiple times in the array.
Example: For nums = [1, 2, 1], when processing the second 1, we're adding to the already updated sum_ending_at[1] from the first 1, which might not be the intended behavior.
Solution: Calculate the new values first, then update the dictionaries:
def sumOfGoodSubsequences(self, nums: List[int]) -> int:
    MOD = 10**9 + 7
    sum_ending_at = defaultdict(int)
    count_ending_at = defaultdict(int)
  
    for current_value in nums:
        # Calculate contributions from extending existing subsequences
        extend_sum = 0
        extend_count = 0
      
        # Extensions from value - 1
        if current_value - 1 in sum_ending_at:
            extend_sum += sum_ending_at[current_value - 1] + count_ending_at[current_value - 1] * current_value
            extend_count += count_ending_at[current_value - 1]
      
        # Extensions from value + 1
        if current_value + 1 in sum_ending_at:
            extend_sum += sum_ending_at[current_value + 1] + count_ending_at[current_value + 1] * current_value
            extend_count += count_ending_at[current_value + 1]
      
        # Update with new subsequence starting at current_value plus extensions
        sum_ending_at[current_value] = (sum_ending_at[current_value] + current_value + extend_sum) % MOD
        count_ending_at[current_value] = (count_ending_at[current_value] + 1 + extend_count) % MOD
  
    return sum(sum_ending_at.values()) % MOD
Pitfall 3: Memory Issues with Very Large Value Ranges
Problem: If the input array contains values with a very large range (e.g., -10^9 to 10^9), the dictionaries might consume excessive memory even with sparse data.
Solution: Consider value normalization or compression if the actual number of distinct values is much smaller than the range:
def sumOfGoodSubsequences(self, nums: List[int]) -> int:
    MOD = 10**9 + 7
  
    # Compress values if needed
    unique_vals = sorted(set(nums))
    if len(unique_vals) < len(nums) // 2:  # Only compress if beneficial
        val_map = {v: i for i, v in enumerate(unique_vals)}
        compressed = [val_map[x] for x in nums]
        # Process with compressed values, then map back results
  
    # Continue with regular processing...
In a binary min heap, the maximum element can be found in:
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
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
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!