2386. Find the K-Sum of an Array
Problem Description
You have an integer array nums
and a positive integer k
. Your task is to find the k-th largest sum among all possible subsequence sums of the array.
A subsequence is formed by selecting any number of elements from the array (including zero elements or all elements) while maintaining their original order. For example, if nums = [1, 2, 3]
, then [1, 3]
, [2]
, []
, and [1, 2, 3]
are all valid subsequences.
The K-Sum is defined as the k-th largest value when you list all possible subsequence sums in descending order. Note that:
- The empty subsequence has a sum of
0
- The same sum value can appear multiple times if different subsequences produce it (they are counted separately in the ranking)
For instance, if nums = [2, 4, -2]
and k = 5
:
- All possible subsequence sums are:
6
(from[2, 4]
),4
(from[4]
),2
(from[2]
),2
(from[4, -2]
),0
(from[]
or[2, -2]
),-2
(from[-2]
) - When sorted in descending order:
6, 4, 2, 2, 0, 0, -2
- The 5th largest sum is
0
Your goal is to return this k-th largest subsequence sum.
Intuition
The key insight is to transform the problem from finding the k-th largest sum to finding the k-th smallest "reduction" from the maximum possible sum.
First, consider what the maximum possible subsequence sum would be - it's simply the sum of all positive numbers in the array. Let's call this mx
. Any other subsequence sum can be viewed as mx
minus some non-negative value (the sum of positive numbers we didn't take plus the absolute value of negative numbers we did take).
For example, if nums = [3, -1, 2]
, the maximum sum is 3 + 2 = 5
. If we want the subsequence [3, -1]
, its sum is 2
, which equals 5 - 3
where 3
is the cost of not taking 2
and taking -1
.
This transformation is powerful because:
- The 1st largest sum corresponds to the smallest reduction (0 reduction gives us
mx
) - The 2nd largest sum corresponds to the 2nd smallest reduction
- The k-th largest sum corresponds to the k-th smallest reduction
Now we need to find the k-th smallest reduction efficiently. We can reformulate the array by replacing all negative numbers with their absolute values and sorting. This way, selecting elements from this transformed array represents the "cost" or "reduction" from the maximum sum.
To avoid generating all possible subsequences (which would be 2^n
), we use a min-heap with a clever expansion strategy. Starting with an empty subsequence (0 reduction), we expand by considering:
- Including the next element in our subsequence
- Swapping the last included element with the next element (excluding the previous and including the current)
This approach ensures we explore subsequence sums in increasing order of their reduction from mx
, allowing us to find the k-th smallest reduction in O(k log k)
time rather than exploring all possibilities.
Learn more about Sorting and Heap (Priority Queue) patterns.
Solution Approach
The implementation follows the intuition by first calculating the maximum possible sum and then using a min-heap to find the k-th smallest reduction.
Step 1: Transform the array and find maximum sum
mx = 0
for i, x in enumerate(nums):
if x > 0:
mx += x
else:
nums[i] = -x
We iterate through nums
and:
- Add all positive numbers to
mx
(the maximum possible sum) - Replace negative numbers with their absolute values
This transformation allows us to treat all elements uniformly as "costs" that reduce from the maximum sum.
Step 2: Sort the transformed array
nums.sort()
Sorting in ascending order ensures that we can systematically explore subsequences with increasing reductions.
Step 3: Use a min-heap to find k-th smallest reduction
h = [(0, 0)]
for _ in range(k - 1):
s, i = heappop(h)
if i < len(nums):
heappush(h, (s + nums[i], i + 1))
if i:
heappush(h, (s + nums[i] - nums[i - 1], i + 1))
The heap stores tuples (s, i)
where:
s
is the current reduction sumi
is the index of the next element to consider
Starting with (0, 0)
(empty subsequence, no reduction), we perform k-1
iterations:
- Pop the smallest reduction
(s, i)
from the heap - If
i < len(nums)
, we can still add more elements:- Push
(s + nums[i], i + 1)
: Includenums[i]
in the subsequence - If
i > 0
, push(s + nums[i] - nums[i-1], i + 1)
: Replacenums[i-1]
withnums[i]
(swap the last included element)
- Push
This expansion strategy ensures that:
- We never miss any possible subsequence
- We explore them in order of increasing reduction
- We avoid redundant states
Step 4: Return the result
return mx - h[0][0]
After k-1
iterations, the top of the heap contains the k-th smallest reduction. The k-th largest sum is mx
minus this reduction.
The time complexity is O(n log n + k log k)
where n
is the array length (for sorting) and k
iterations with heap operations. The space complexity is O(k)
for the heap.
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 solution with nums = [2, -3, 5]
and k = 4
.
Step 1: Transform array and find maximum sum
- Original:
[2, -3, 5]
mx = 2 + 5 = 7
(sum of positive numbers)- Transform negative numbers:
[2, 3, 5]
(replaced-3
with3
)
Step 2: Sort the transformed array
- Sorted:
[2, 3, 5]
Step 3: Use min-heap to find 4th smallest reduction
Initial heap: [(0, 0)]
(no reduction, start at index 0)
Iteration 1 (finding 1st smallest reduction):
- Pop
(0, 0)
from heap - Since
i = 0 < 3
:- Push
(0 + 2, 1) = (2, 1)
(include element at index 0) - Skip second push since
i = 0
- Push
- Heap:
[(2, 1)]
Iteration 2 (finding 2nd smallest reduction):
- Pop
(2, 1)
from heap - Since
i = 1 < 3
:- Push
(2 + 3, 2) = (5, 2)
(include element at index 1) - Push
(2 + 3 - 2, 2) = (3, 2)
(swap elements at indices 0 and 1)
- Push
- Heap:
[(3, 2), (5, 2)]
Iteration 3 (finding 3rd smallest reduction):
- Pop
(3, 2)
from heap - Since
i = 2 < 3
:- Push
(3 + 5, 3) = (8, 3)
(include element at index 2) - Push
(3 + 5 - 3, 3) = (5, 3)
(swap elements at indices 1 and 2)
- Push
- Heap:
[(5, 2), (5, 3), (8, 3)]
After 3 iterations, the heap's minimum is (5, 2)
, which is the 4th smallest reduction.
Step 4: Return result
- 4th largest sum =
mx - reduction = 7 - 5 = 2
Verification:
All possible subsequence sums for [2, -3, 5]
:
[2, 5]
→ sum = 7[5]
→ sum = 5[2]
→ sum = 2[2, -3, 5]
→ sum = 4[]
→ sum = 0[-3, 5]
→ sum = 2[-3]
→ sum = -3[2, -3]
→ sum = -1
Sorted descending: 7, 5, 4, 2, 2, 0, -1, -3
The 4th largest is indeed 2
✓
Solution Implementation
1class Solution:
2 def kSum(self, nums: List[int], k: int) -> int:
3 # Calculate the maximum possible sum (sum of all positive numbers)
4 max_possible_sum = 0
5 for i, num in enumerate(nums):
6 if num > 0:
7 max_possible_sum += num
8 else:
9 # Convert negative numbers to positive for unified handling
10 nums[i] = -num
11
12 # Sort the absolute values in ascending order
13 nums.sort()
14
15 # Min-heap to track the k smallest "reduction amounts" from max_possible_sum
16 # Each tuple contains: (reduction_sum, next_index_to_consider)
17 min_heap = [(0, 0)]
18
19 # Find the (k-1)th smallest reduction to get the k-th largest sum
20 for _ in range(k - 1):
21 current_reduction, current_index = heappop(min_heap)
22
23 if current_index < len(nums):
24 # Option 1: Include nums[current_index] in the reduction
25 heappush(min_heap, (current_reduction + nums[current_index], current_index + 1))
26
27 # Option 2: Replace nums[current_index - 1] with nums[current_index]
28 # This only makes sense if we previously included nums[current_index - 1]
29 if current_index > 0:
30 heappush(min_heap, (current_reduction + nums[current_index] - nums[current_index - 1], current_index + 1))
31
32 # The k-th largest sum = max_possible_sum - (k-1)th smallest reduction
33 return max_possible_sum - min_heap[0][0]
34
1class Solution {
2 public long kSum(int[] nums, int k) {
3 // Calculate the maximum possible sum (sum of all positive numbers)
4 // and convert all numbers to their absolute values
5 long maxSum = 0;
6 int n = nums.length;
7
8 for (int i = 0; i < n; i++) {
9 if (nums[i] > 0) {
10 maxSum += nums[i];
11 } else {
12 nums[i] = -nums[i]; // Convert negative numbers to positive
13 }
14 }
15
16 // Sort the absolute values in ascending order
17 Arrays.sort(nums);
18
19 // Priority queue to store pairs of (sum reduction, next index to consider)
20 // Sorted by sum reduction in ascending order
21 PriorityQueue<Pair<Long, Integer>> minHeap =
22 new PriorityQueue<>(Comparator.comparing(Pair::getKey));
23
24 // Start with no reduction (empty subset removed from maxSum)
25 minHeap.offer(new Pair<>(0L, 0));
26
27 // Find the k-th smallest reduction
28 while (--k > 0) {
29 Pair<Long, Integer> current = minHeap.poll();
30 long currentReduction = current.getKey();
31 int nextIndex = current.getValue();
32
33 if (nextIndex < n) {
34 // Option 1: Include nums[nextIndex] in the reduction
35 minHeap.offer(new Pair<>(currentReduction + nums[nextIndex], nextIndex + 1));
36
37 // Option 2: Replace nums[nextIndex-1] with nums[nextIndex] in the reduction
38 // (only valid if we had previously included nums[nextIndex-1])
39 if (nextIndex > 0) {
40 minHeap.offer(new Pair<>(
41 currentReduction + nums[nextIndex] - nums[nextIndex - 1],
42 nextIndex + 1
43 ));
44 }
45 }
46 }
47
48 // The k-th largest sum is maxSum minus the k-th smallest reduction
49 return maxSum - minHeap.peek().getKey();
50 }
51}
52
1class Solution {
2public:
3 long long kSum(vector<int>& nums, int k) {
4 // Calculate the maximum possible sum (sum of all positive numbers)
5 // and convert all numbers to their absolute values
6 long long maxSum = 0;
7 int n = nums.size();
8
9 for (int i = 0; i < n; ++i) {
10 if (nums[i] > 0) {
11 maxSum += nums[i]; // Add positive numbers to max sum
12 } else {
13 nums[i] *= -1; // Convert negative numbers to positive
14 }
15 }
16
17 // Sort the absolute values in ascending order
18 sort(nums.begin(), nums.end());
19
20 // Min heap to store pairs of (reduction_sum, next_index)
21 // The reduction sum represents how much we subtract from maxSum
22 using PairLongInt = pair<long long, int>;
23 priority_queue<PairLongInt, vector<PairLongInt>, greater<PairLongInt>> minHeap;
24
25 // Start with 0 reduction (empty subsequence to exclude)
26 minHeap.push({0, 0});
27
28 // Find the k-th smallest reduction
29 while (--k) {
30 // Get the smallest reduction so far
31 auto current = minHeap.top();
32 minHeap.pop();
33
34 long long currentReduction = current.first;
35 int currentIndex = current.second;
36
37 // If we haven't processed all elements yet
38 if (currentIndex < n) {
39 // Option 1: Include nums[currentIndex] in the reduction
40 // (exclude it from the sum)
41 minHeap.push({currentReduction + nums[currentIndex], currentIndex + 1});
42
43 // Option 2: Replace the previous element with current element
44 // This avoids generating duplicate subsequences
45 if (currentIndex > 0) {
46 minHeap.push({currentReduction + nums[currentIndex] - nums[currentIndex - 1],
47 currentIndex + 1});
48 }
49 }
50 }
51
52 // The k-th largest sum = maxSum - (k-th smallest reduction)
53 return maxSum - minHeap.top().first;
54 }
55};
56
1function kSum(nums: number[], k: number): number {
2 // Calculate the maximum possible sum (sum of all positive numbers)
3 // and convert all numbers to their absolute values
4 let maxSum: number = 0;
5 const n: number = nums.length;
6
7 for (let i = 0; i < n; i++) {
8 if (nums[i] > 0) {
9 maxSum += nums[i]; // Add positive numbers to max sum
10 } else {
11 nums[i] *= -1; // Convert negative numbers to positive
12 }
13 }
14
15 // Sort the absolute values in ascending order
16 nums.sort((a, b) => a - b);
17
18 // Min heap to store pairs of [reductionSum, nextIndex]
19 // The reduction sum represents how much we subtract from maxSum
20 // Using an array to simulate a min heap with custom comparison
21 const minHeap: Array<[number, number]> = [];
22
23 // Helper function to maintain heap property when pushing
24 const heapPush = (item: [number, number]): void => {
25 minHeap.push(item);
26 let currentIdx = minHeap.length - 1;
27
28 // Bubble up to maintain min heap property
29 while (currentIdx > 0) {
30 const parentIdx = Math.floor((currentIdx - 1) / 2);
31 if (minHeap[currentIdx][0] < minHeap[parentIdx][0]) {
32 [minHeap[currentIdx], minHeap[parentIdx]] = [minHeap[parentIdx], minHeap[currentIdx]];
33 currentIdx = parentIdx;
34 } else {
35 break;
36 }
37 }
38 };
39
40 // Helper function to extract minimum from heap
41 const heapPop = (): [number, number] => {
42 const min = minHeap[0];
43 const last = minHeap.pop()!;
44
45 if (minHeap.length > 0) {
46 minHeap[0] = last;
47 let currentIdx = 0;
48
49 // Bubble down to maintain min heap property
50 while (true) {
51 let smallestIdx = currentIdx;
52 const leftChild = 2 * currentIdx + 1;
53 const rightChild = 2 * currentIdx + 2;
54
55 if (leftChild < minHeap.length && minHeap[leftChild][0] < minHeap[smallestIdx][0]) {
56 smallestIdx = leftChild;
57 }
58 if (rightChild < minHeap.length && minHeap[rightChild][0] < minHeap[smallestIdx][0]) {
59 smallestIdx = rightChild;
60 }
61
62 if (smallestIdx !== currentIdx) {
63 [minHeap[currentIdx], minHeap[smallestIdx]] = [minHeap[smallestIdx], minHeap[currentIdx]];
64 currentIdx = smallestIdx;
65 } else {
66 break;
67 }
68 }
69 }
70
71 return min;
72 };
73
74 // Start with 0 reduction (empty subsequence to exclude)
75 heapPush([0, 0]);
76
77 // Find the k-th smallest reduction
78 while (--k) {
79 // Get the smallest reduction so far
80 const [currentReduction, currentIndex] = heapPop();
81
82 // If we haven't processed all elements yet
83 if (currentIndex < n) {
84 // Option 1: Include nums[currentIndex] in the reduction
85 // (exclude it from the sum)
86 heapPush([currentReduction + nums[currentIndex], currentIndex + 1]);
87
88 // Option 2: Replace the previous element with current element
89 // This avoids generating duplicate subsequences
90 if (currentIndex > 0) {
91 heapPush([
92 currentReduction + nums[currentIndex] - nums[currentIndex - 1],
93 currentIndex + 1
94 ]);
95 }
96 }
97 }
98
99 // The k-th largest sum = maxSum - (k-th smallest reduction)
100 return maxSum - minHeap[0][0];
101}
102
Time and Space Complexity
Time Complexity: O(n × log n + k × log k)
The time complexity consists of two main parts:
O(n × log n)
for sorting thenums
array, wheren
is the length of the arrayO(k × log k)
for the heap operations:- The loop runs
k - 1
times - Each iteration performs one
heappop
operation (O(log h)
whereh
is heap size) and at most twoheappush
operations (alsoO(log h)
each) - The heap size is bounded by
O(k)
since we only push at most 2 elements per iteration and pop 1 element - Therefore, the total heap operations cost is
O(k × log k)
- The loop runs
Space Complexity: O(n)
The space complexity is determined by:
O(n)
for storing the modifiednums
array (in-place modification)O(k)
for the heaph
, which can contain at mostO(k)
elements- Since
k ≤ n
(we can't find more thann
sums), and we needO(n)
space for the array, the overall space complexity isO(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Heap Expansion Logic
Pitfall: The most common mistake is not understanding why we need both expansion options when popping from the heap. Developers often think adding just (s + nums[i], i + 1)
is sufficient.
Why it's wrong: Using only one expansion option will miss many valid subsequences. For example, if nums = [1, 2, 3]
and we only use the "include" option, we'd generate subsequences like []
, [1]
, [1,2]
, [1,2,3]
but miss [2]
, [3]
, [2,3]
, etc.
Solution: Always include both expansion options:
# Option 1: Include the current element heappush(min_heap, (current_reduction + nums[current_index], current_index + 1)) # Option 2: Replace the previous element with current (if previous exists) if current_index > 0: heappush(min_heap, (current_reduction + nums[current_index] - nums[current_index - 1], current_index + 1))
2. Incorrectly Handling Negative Numbers
Pitfall: Forgetting to transform negative numbers or transforming them incorrectly. Some might try to simply ignore negative numbers or handle them separately.
Why it's wrong: Negative numbers can be part of subsequences and affect the final sum. Simply ignoring them would give incorrect results.
Solution: Transform all numbers to their absolute values after calculating the maximum sum:
for i, num in enumerate(nums):
if num > 0:
max_possible_sum += num
else:
nums[i] = -num # Convert to positive for unified handling
3. Off-by-One Error in Loop Count
Pitfall: Running the heap expansion loop k
times instead of k-1
times.
Why it's wrong: The initial state (0, 0)
already represents the first (largest) subsequence sum. We only need k-1
more iterations to find the k-th largest.
Solution: Use range(k - 1)
for the loop:
# Start with the maximum sum (reduction = 0)
min_heap = [(0, 0)]
# Find k-1 more sums to get to the k-th largest
for _ in range(k - 1):
# ... heap operations
4. Heap State Duplication
Pitfall: Not realizing that the same reduction sum might be reached through different paths, potentially leading to duplicate counting or inefficiency.
Why it's acceptable here: While duplicates can occur, they represent different subsequences that happen to have the same sum. Since we want the k-th largest sum (not k distinct sums), counting duplicates is correct. The algorithm naturally handles this by treating each path independently.
Note: If you needed k distinct sums, you'd need to track visited states using a set:
visited = set()
while heap and len(result) < k:
reduction, index = heappop(heap)
if (reduction, index) in visited:
continue
visited.add((reduction, index))
# ... rest of logic
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
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
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
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!