3098. Find the Sum of Subsequence Powers
Problem Description
You are given an integer array nums
of length n
and a positive integer k
.
The power of a subsequence is defined as the minimum absolute difference between any two elements in that subsequence.
Your task is to find the sum of powers of all subsequences of nums
that have exactly k
elements.
Since the answer may be very large, return it modulo 10^9 + 7
.
Key Points:
- A subsequence is formed by deleting some (possibly zero) elements from the array without changing the order of the remaining elements
- You need to consider all possible subsequences of length exactly
k
- For each such subsequence, calculate its power (the minimum absolute difference between any two elements in it)
- Sum up all these powers and return the result modulo
10^9 + 7
For example, if you have a subsequence [a, b, c]
, its power would be min(|a-b|, |a-c|, |b-c|)
.
Intuition
When dealing with minimum differences between elements, sorting the array first is a natural approach. Once sorted, the minimum difference in any subsequence will always be between two consecutive elements (in their sorted positions within the subsequence). This is because for any two elements in a sorted subsequence, their difference increases as they get further apart.
The key insight is that we need to track not just which elements we've selected for our subsequence, but also what the current minimum difference is. As we build subsequences, this minimum difference might change based on which elements we include.
We can think of this as a dynamic programming problem where at each position, we make a choice: either include the current element or skip it. However, when we include an element, we need to know:
- What was the last element we included (to calculate the new difference)
- How many more elements we still need to select
- What is the current minimum difference in our subsequence so far
This leads us to define our state as (i, j, k, mi)
where:
i
is the current position we're considering in the sorted arrayj
is the index of the last selected element (orn
if no element has been selected yet)k
is the number of elements we still need to selectmi
is the current minimum difference in our subsequence
For each state, we have two choices:
- Skip the current element: move to the next position without changing other parameters
- Include the current element: update the last selected element to
i
, decreasek
by 1, and potentially update the minimum difference (ifj
is notn
, the new difference would benums[i] - nums[j]
)
The power of each complete subsequence (when k
reaches 0) is its minimum difference mi
, and we sum all these powers across all valid subsequences.
Learn more about Dynamic Programming and Sorting patterns.
Solution Approach
The solution uses memoization search (top-down dynamic programming) to efficiently compute the sum of powers for all subsequences of length k
.
Step 1: Sort the array
nums.sort()
Sorting ensures that when we calculate differences between selected elements, consecutive selected elements in the sorted order will give us potential minimum differences.
Step 2: Define the DFS function with memoization
@cache
def dfs(i: int, j: int, k: int, mi: int) -> int:
The function parameters represent:
i
: Current index in the sorted array we're consideringj
: Index of the last selected element (initialized ton
meaning no element selected yet)k
: Number of elements still needed to complete the subsequencemi
: Current minimum difference in the subsequence being built
Step 3: Base cases
if i >= n: return mi if k == 0 else 0 if n - i < k: return 0
- If we've processed all elements (
i >= n
), returnmi
only if we've selected exactlyk
elements (k == 0
) - If remaining elements are insufficient (
n - i < k
), it's impossible to form a valid subsequence, return 0
Step 4: Recursive transitions
ans = dfs(i + 1, j, k, mi) # Skip current element
First, calculate the result when we don't select the current element.
if j == n:
ans += dfs(i + 1, i, k - 1, mi)
else:
ans += dfs(i + 1, i, k - 1, min(mi, nums[i] - nums[j]))
Then add the result when we select the current element:
- If
j == n
(no previous element selected), we start a new subsequence with element at indexi
- Otherwise, we update the minimum difference to
min(mi, nums[i] - nums[j])
sincenums[i] - nums[j]
is the difference between the current and last selected element
Step 5: Return the result with modulo
ans %= mod return ans
Step 6: Initialize and call the function
mod = 10**9 + 7
n = len(nums)
return dfs(0, n, k, inf)
Start from index 0, with no element selected (j = n
), needing to select k
elements, and initial minimum difference as infinity.
The @cache
decorator automatically memoizes the results, preventing redundant calculations for the same state (i, j, k, mi)
. This optimization is crucial as there can be many overlapping subproblems in the recursive calls.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example to understand how the solution works.
Example: nums = [1, 3, 5]
, k = 2
Step 1: Sort the array
After sorting: nums = [1, 3, 5]
(already sorted)
Step 2: Initialize and start DFS
We call dfs(0, 3, 2, inf)
where:
i = 0
: Starting at index 0j = 3
: No element selected yet (using n=3 as sentinel)k = 2
: Need to select 2 elementsmi = inf
: Initial minimum difference is infinity
Step 3: Trace the recursion
From dfs(0, 3, 2, inf)
:
-
Option 1: Skip nums[0]=1
- Call
dfs(1, 3, 2, inf)
- Call
-
Option 2: Select nums[0]=1
- Since
j = 3
(no previous element), don't update mi - Call
dfs(1, 0, 1, inf)
- Since
Let's explore Option 2 first: dfs(1, 0, 1, inf)
- Current: index 1, last selected was index 0 (value 1), need 1 more element
- Option 2a: Skip nums[1]=3
- Call
dfs(2, 0, 1, inf)
- Call
- Option 2b: Select nums[1]=3
- New difference:
nums[1] - nums[0] = 3 - 1 = 2
- Update mi:
min(inf, 2) = 2
- Call
dfs(2, 1, 0, 2)
- New difference:
From dfs(2, 1, 0, 2)
:
- We need 0 more elements (k=0), so we've completed a subsequence
- Continue to
dfs(3, 1, 0, 2)
- Since
i >= n
, returnmi = 2
(subsequence [1,3] has power 2)
From dfs(2, 0, 1, inf)
:
- Select nums[2]=5 (only valid option as we need 1 more)
- New difference:
nums[2] - nums[0] = 5 - 1 = 4
- Update mi:
min(inf, 4) = 4
- Call
dfs(3, 2, 0, 4)
- Returns 4 (subsequence [1,5] has power 4)
- New difference:
Back to Option 1: dfs(1, 3, 2, inf)
- Skip nums[1]=3: Call
dfs(2, 3, 2, inf)
→ Returns 0 (can't select 2 from 1 element) - Select nums[1]=3: Call
dfs(2, 1, 1, inf)
- Must select nums[2]=5
- New difference:
5 - 3 = 2
- Returns 2 (subsequence [3,5] has power 2)
Step 4: Sum all powers
- Subsequence [1,3]: power = 2
- Subsequence [1,5]: power = 4
- Subsequence [3,5]: power = 2
- Total sum = 2 + 4 + 2 = 8
The algorithm correctly identifies all subsequences of length 2 and calculates their powers (minimum absolute differences), then returns their sum modulo 10^9 + 7.
Solution Implementation
1from typing import List
2from functools import cache
3from math import inf
4
5class Solution:
6 def sumOfPowers(self, nums: List[int], k: int) -> int:
7 MOD = 10**9 + 7
8 n = len(nums)
9
10 # Sort the array to make it easier to calculate differences
11 nums.sort()
12
13 @cache
14 def dfs(current_index: int, last_picked_index: int, remaining_count: int, min_difference: int) -> int:
15 """
16 Dynamic programming function to calculate sum of powers.
17
18 Args:
19 current_index: Current position in nums array
20 last_picked_index: Index of the last element picked in subsequence
21 remaining_count: Number of elements still needed for subsequence
22 min_difference: Minimum difference seen so far in current subsequence
23
24 Returns:
25 Sum of powers for all valid subsequences
26 """
27 # Base case: reached end of array
28 if current_index >= n:
29 # Return min_difference if we've selected exactly k elements
30 return min_difference if remaining_count == 0 else 0
31
32 # Pruning: not enough elements left to form subsequence of size k
33 if n - current_index < remaining_count:
34 return 0
35
36 # Option 1: Skip current element
37 result = dfs(current_index + 1, last_picked_index, remaining_count, min_difference)
38
39 # Option 2: Include current element
40 if last_picked_index == n:
41 # First element in subsequence
42 result += dfs(current_index + 1, current_index, remaining_count - 1, min_difference)
43 else:
44 # Calculate new minimum difference when adding current element
45 new_min_difference = min(min_difference, nums[current_index] - nums[last_picked_index])
46 result += dfs(current_index + 1, current_index, remaining_count - 1, new_min_difference)
47
48 # Apply modulo operation
49 result %= MOD
50
51 return result
52
53 # Start DFS with initial parameters
54 # current_index=0, last_picked_index=n (sentinel for no element picked yet),
55 # remaining_count=k, min_difference=inf
56 return dfs(0, n, k, inf)
57
1class Solution {
2 // Memoization cache to store computed results
3 private Map<Long, Integer> memo = new HashMap<>();
4
5 // Modulo value for the answer
6 private final int MOD = (int) 1e9 + 7;
7
8 // Reference to the sorted input array
9 private int[] nums;
10
11 /**
12 * Main method to calculate the sum of powers
13 * @param nums - input array of numbers
14 * @param k - size of subsequences to consider
15 * @return sum of minimum differences for all subsequences of size k
16 */
17 public int sumOfPowers(int[] nums, int k) {
18 // Sort the array to ensure elements are in ascending order
19 Arrays.sort(nums);
20 this.nums = nums;
21
22 // Start DFS with initial parameters
23 // i=0: starting index
24 // j=nums.length: indicates no previous element selected
25 // k: remaining elements to select
26 // mi=Integer.MAX_VALUE: current minimum difference
27 return dfs(0, nums.length, k, Integer.MAX_VALUE);
28 }
29
30 /**
31 * Recursive DFS function with memoization
32 * @param currentIndex - current position in the array
33 * @param previousIndex - index of the previously selected element (nums.length if none)
34 * @param remainingCount - number of elements still needed to select
35 * @param minDifference - minimum difference found so far in current subsequence
36 * @return sum of minimum differences for all valid subsequences
37 */
38 private int dfs(int currentIndex, int previousIndex, int remainingCount, int minDifference) {
39 // Base case: reached end of array
40 if (currentIndex >= nums.length) {
41 // If we've selected exactly k elements, return the minimum difference
42 // Otherwise, this is an invalid subsequence, return 0
43 return remainingCount == 0 ? minDifference : 0;
44 }
45
46 // Pruning: not enough elements left to form a subsequence of size k
47 if (nums.length - currentIndex < remainingCount) {
48 return 0;
49 }
50
51 // Create unique key for memoization
52 // Encode state: minDifference (bits 18-31), currentIndex (bits 12-17),
53 // previousIndex (bits 6-11), remainingCount (bits 0-5)
54 long stateKey = (1L * minDifference) << 18 | (currentIndex << 12) | (previousIndex << 6) | remainingCount;
55
56 // Check if this state has been computed before
57 if (memo.containsKey(stateKey)) {
58 return memo.get(stateKey);
59 }
60
61 // Option 1: Skip current element
62 int result = dfs(currentIndex + 1, previousIndex, remainingCount, minDifference);
63
64 // Option 2: Include current element in the subsequence
65 if (previousIndex == nums.length) {
66 // This is the first element being selected
67 result += dfs(currentIndex + 1, currentIndex, remainingCount - 1, minDifference);
68 } else {
69 // Update minimum difference with the difference between current and previous element
70 int newMinDifference = Math.min(minDifference, nums[currentIndex] - nums[previousIndex]);
71 result += dfs(currentIndex + 1, currentIndex, remainingCount - 1, newMinDifference);
72 }
73
74 // Apply modulo to prevent overflow
75 result %= MOD;
76
77 // Store result in memoization cache
78 memo.put(stateKey, result);
79
80 return result;
81 }
82}
83
1class Solution {
2public:
3 int sumOfPowers(vector<int>& nums, int k) {
4 // Memoization cache: key encodes (minDiff, currentIndex, previousIndex, remaining)
5 unordered_map<long long, int> memo;
6 const int MOD = 1e9 + 7;
7 int n = nums.size();
8
9 // Sort array to ensure we can calculate differences between adjacent selected elements
10 sort(nums.begin(), nums.end());
11
12 // DFS with memoization to find sum of minimum differences
13 // Parameters:
14 // - currentIdx: current position in nums array
15 // - previousIdx: index of last selected element (n means no element selected yet)
16 // - remaining: number of elements still needed to select
17 // - minDiff: current minimum difference in the subsequence
18 auto dfs = [&](this auto&& dfs, int currentIdx, int previousIdx, int remaining, int minDiff) -> int {
19 // Base case: reached end of array
20 if (currentIdx >= n) {
21 // Return minDiff if we selected exactly k elements, otherwise 0
22 return remaining == 0 ? minDiff : 0;
23 }
24
25 // Pruning: not enough elements left to select
26 if (n - currentIdx < remaining) {
27 return 0;
28 }
29
30 // Create unique key for memoization
31 // Bit packing: minDiff (high bits), currentIdx (12-17), previousIdx (6-11), remaining (0-5)
32 long long stateKey = (1LL * minDiff) << 18 | (currentIdx << 12) | (previousIdx << 6) | remaining;
33
34 // Check if state already computed
35 if (memo.contains(stateKey)) {
36 return memo[stateKey];
37 }
38
39 // Option 1: Skip current element
40 long long totalSum = dfs(currentIdx + 1, previousIdx, remaining, minDiff);
41
42 // Option 2: Include current element in subsequence
43 if (previousIdx == n) {
44 // First element in subsequence, no difference to calculate
45 totalSum += dfs(currentIdx + 1, currentIdx, remaining - 1, minDiff);
46 } else {
47 // Calculate difference with previous selected element and update minimum
48 int currentDiff = nums[currentIdx] - nums[previousIdx];
49 totalSum += dfs(currentIdx + 1, currentIdx, remaining - 1, min(minDiff, currentDiff));
50 }
51
52 // Apply modulo and store result
53 totalSum %= MOD;
54 memo[stateKey] = totalSum;
55
56 return memo[stateKey];
57 };
58
59 // Start DFS: index 0, no previous element (n), need k elements, initial minDiff is INT_MAX
60 return dfs(0, n, k, INT_MAX);
61 }
62};
63
1/**
2 * Calculates the sum of powers for all subsequences of length k
3 * @param nums - Array of numbers to process
4 * @param k - Required subsequence length
5 * @returns Sum of minimum differences modulo 10^9 + 7
6 */
7function sumOfPowers(nums: number[], k: number): number {
8 const MOD = BigInt(1e9 + 7);
9
10 // Sort array in ascending order for easier processing
11 nums.sort((a, b) => a - b);
12 const arrayLength = nums.length;
13
14 // Memoization cache using encoded state as key
15 const memoCache: Map<bigint, bigint> = new Map();
16
17 /**
18 * Dynamic programming helper function to calculate sum recursively
19 * @param currentIndex - Current position in the array
20 * @param previousIndex - Index of the last selected element (arrayLength means none selected)
21 * @param remainingCount - Number of elements still needed to select
22 * @param minDifference - Current minimum difference between consecutive selected elements
23 * @returns Sum of minimum differences for valid subsequences
24 */
25 function calculateSum(
26 currentIndex: number,
27 previousIndex: number,
28 remainingCount: number,
29 minDifference: number
30 ): bigint {
31 // Base case: reached end of array
32 if (currentIndex >= arrayLength) {
33 // Valid subsequence found if we selected exactly k elements
34 if (remainingCount === 0) {
35 return BigInt(minDifference);
36 }
37 return BigInt(0);
38 }
39
40 // Pruning: not enough elements left to form subsequence of size k
41 if (arrayLength - currentIndex < remainingCount) {
42 return BigInt(0);
43 }
44
45 // Create unique key for memoization by encoding all state parameters
46 // Using bit shifting to pack multiple values into single bigint
47 const stateKey =
48 (BigInt(minDifference) << BigInt(18)) |
49 (BigInt(currentIndex) << BigInt(12)) |
50 (BigInt(previousIndex) << BigInt(6)) |
51 BigInt(remainingCount);
52
53 // Check memoization cache
54 if (memoCache.has(stateKey)) {
55 return memoCache.get(stateKey)!;
56 }
57
58 // Option 1: Skip current element
59 let totalSum = calculateSum(
60 currentIndex + 1,
61 previousIndex,
62 remainingCount,
63 minDifference
64 );
65
66 // Option 2: Include current element in subsequence
67 if (previousIndex === arrayLength) {
68 // First element being selected, no difference to calculate
69 totalSum += calculateSum(
70 currentIndex + 1,
71 currentIndex,
72 remainingCount - 1,
73 minDifference
74 );
75 } else {
76 // Calculate difference with previous selected element
77 // Update minimum difference if current difference is smaller
78 totalSum += calculateSum(
79 currentIndex + 1,
80 currentIndex,
81 remainingCount - 1,
82 Math.min(minDifference, nums[currentIndex] - nums[previousIndex])
83 );
84 }
85
86 // Apply modulo operation
87 totalSum %= MOD;
88
89 // Store result in cache
90 memoCache.set(stateKey, totalSum);
91
92 return totalSum;
93 }
94
95 // Start recursion from index 0, with no previous selection, needing k elements
96 // Initial minimum difference is set to maximum safe integer
97 return Number(calculateSum(0, arrayLength, k, Number.MAX_SAFE_INTEGER));
98}
99
Time and Space Complexity
The time complexity is O(n^3 × k)
and the space complexity is O(n^3 × k)
.
Let me analyze the recursive function dfs(i, j, k, mi)
:
- Parameter
i
ranges from0
ton-1
(n possible values) - Parameter
j
ranges from0
ton
(n+1 possible values, including the initial valuen
) - Parameter
k
ranges from0
tok
(k+1 possible values) - Parameter
mi
can take at mostn^2
different values (all possible differences between pairs of elements in the sorted array, plus infinity)
However, upon closer examination, mi
represents the minimum difference encountered so far. In a sorted array of length n
, there are at most n-1
consecutive differences, and when considering all possible subsequences, the number of unique minimum differences is bounded by O(n^2)
(all pairwise differences).
Therefore, the total number of unique states in the memoization is O(n × n × k × n^2) = O(n^4 × k)
.
Each state computation involves constant time operations (excluding recursive calls), so the time complexity is O(n^4 × k)
.
The space complexity is also O(n^4 × k)
due to the memoization cache storing all possible states.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Using Infinity as Initial Minimum Difference
Problem:
Using float('inf')
as the initial minimum difference can cause issues with hashing in the @cache
decorator. Python's functools.cache
uses the arguments as dictionary keys, and float('inf')
may not hash consistently across different Python implementations or could lead to unexpected behavior in the memoization.
Solution: Use a large integer value instead of infinity as the initial minimum difference:
# Instead of: return dfs(0, n, k, inf) # Use: MAX_DIFF = 10**9 # or max(nums) - min(nums) + 1 return dfs(0, n, k, MAX_DIFF)
Pitfall 2: Incorrect Handling of Minimum Difference Updates
Problem: A common mistake is updating the minimum difference incorrectly when selecting elements. Since the array is sorted, we only need to consider the difference between consecutive selected elements (not all pairs), but developers might try to compare the current element with all previously selected elements.
Incorrect approach:
# Wrong: Trying to maintain all selected elements and compute all pairwise differences
selected_elements.append(nums[current_index])
new_min = min(abs(nums[current_index] - elem) for elem in selected_elements)
Solution: The correct approach only tracks the last selected element and computes the difference with it:
# Correct: Only compute difference with last selected element
new_min_difference = min(min_difference, nums[current_index] - nums[last_picked_index])
Pitfall 3: Memory Limit Exceeded Due to Excessive State Space
Problem:
If the minimum difference can take too many unique values, the memoization cache can grow exponentially, leading to memory issues. This happens when min_difference
has a large range of possible values.
Solution: Optimize the state representation by discretizing the minimum differences:
def sumOfPowers(self, nums: List[int], k: int) -> int:
MOD = 10**9 + 7
n = len(nums)
nums.sort()
# Collect all possible differences
all_diffs = set()
for i in range(n):
for j in range(i + 1, n):
all_diffs.add(nums[j] - nums[i])
all_diffs.add(float('inf')) # sentinel value
# Create mapping for discretization
diff_to_idx = {diff: idx for idx, diff in enumerate(sorted(all_diffs))}
@cache
def dfs(current_index: int, last_picked_index: int, remaining_count: int, min_diff_idx: int) -> int:
# Use min_diff_idx instead of actual min_difference value
min_difference = sorted(all_diffs)[min_diff_idx]
# ... rest of the logic remains similar
Pitfall 4: Forgetting to Apply Modulo at Each Step
Problem: Only applying modulo at the final return can cause integer overflow in languages with fixed-size integers, or unnecessary large number computations in Python.
Solution: Apply modulo operation after each addition to keep numbers manageable:
result = dfs(current_index + 1, last_picked_index, remaining_count, min_difference) result = (result + dfs(current_index + 1, current_index, remaining_count - 1, new_min_difference)) % MOD return result
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!