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 valuex
g[x]
: tracks the count of all good subsequences ending at valuex
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 valuex
because when we see anotherx
later, we can extend all of them - Sum (
f[x]
): We need the sum of all good subsequences ending withx
to build our final answer
When we process a new element x
:
- We add
x
itself as a new subsequence (count increases by 1, sum increases byx
) - For all subsequences ending at
x-1
, we can appendx
to create new subsequences. If there areg[x-1]
such subsequences, we're addingx
to each of them, contributingg[x-1] * x
to the sum - Similarly for subsequences ending at
x+1
, we addg[x+1] * x
to 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 wheref[x]
stores the sum of all good subsequences ending with valuex
g
: A dictionary whereg[x]
stores the count of all good subsequences ending with valuex
Algorithm Steps:
-
Initialize empty dictionaries
f
andg
usingdefaultdict(int)
to handle missing keys automatically. -
Process each element
x
innums
:a. Add
x
as 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 atx-1
g[x - 1] * x
: contribution of appendingx
to 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 atx+1
g[x + 1] * x
: contribution of appendingx
to each of those subsequences
g[x] += g[x + 1]
(add the count of subsequences we're extending)
-
Calculate final result:
- Sum all values in
f
dictionary:sum(f.values())
- Apply modulo
10^9 + 7
to 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-1
orx+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 valuex
g[x]
: count of all good subsequences ending with valuex
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
31
1class 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}
54
1class 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};
43
1function 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}
38
Time 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+1
in bothf
andg
dictionaries - 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
nums
or is adjacent to a value innums
(i.e.,x-1
orx+1
for somex
innums
) - In the worst case, this creates at most
3k
entries across both dictionaries (for each unique valuex
, we might have entries forx-1
,x
, andx+1
) - Since
3k
is 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...
Which of the following array represent a max heap?
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 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
Want a Structured Path to Master System Design Too? Don’t Miss This!