Facebook Pixel

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|).

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

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:

  1. What was the last element we included (to calculate the new difference)
  2. How many more elements we still need to select
  3. 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 array
  • j is the index of the last selected element (or n if no element has been selected yet)
  • k is the number of elements we still need to select
  • mi is the current minimum difference in our subsequence

For each state, we have two choices:

  1. Skip the current element: move to the next position without changing other parameters
  2. Include the current element: update the last selected element to i, decrease k by 1, and potentially update the minimum difference (if j is not n, the new difference would be nums[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 considering
  • j: Index of the last selected element (initialized to n meaning no element selected yet)
  • k: Number of elements still needed to complete the subsequence
  • mi: 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), return mi only if we've selected exactly k 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 index i
  • Otherwise, we update the minimum difference to min(mi, nums[i] - nums[j]) since nums[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 Evaluator

Example 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 0
  • j = 3: No element selected yet (using n=3 as sentinel)
  • k = 2: Need to select 2 elements
  • mi = 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)
  • Option 2: Select nums[0]=1

    • Since j = 3 (no previous element), don't update mi
    • Call dfs(1, 0, 1, inf)

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)
  • 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)

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, return mi = 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)

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 from 0 to n-1 (n possible values)
  • Parameter j ranges from 0 to n (n+1 possible values, including the initial value n)
  • Parameter k ranges from 0 to k (k+1 possible values)
  • Parameter mi can take at most n^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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More