3684. Maximize Sum of At Most K Distinct Elements
Problem Description
You are given a positive integer array nums and an integer k.
Your task is to choose at most k elements from nums such that their sum is maximized. There is one important restriction: all chosen numbers must be distinct (no duplicates allowed in your selection).
Return an array containing the chosen numbers in strictly descending order.
Key points to understand:
- Since all numbers in
numsare positive, picking more elements always increases the sum. So to maximize the sum, you should pick as many elements as allowed — up tokof them. - Because the chosen numbers must be distinct, if the array contains duplicates, each value can be used only once.
- To make the sum as large as possible, you should greedily pick the largest distinct values available.
- The final answer must be sorted in strictly descending order, which happens naturally when you pick the largest distinct values from biggest to smallest.
Example:
Suppose nums = [5, 3, 5, 8] and k = 3.
- The distinct values are
{8, 5, 3}. - Picking the largest 3 distinct values gives
[8, 5, 3]with sum16, which is the maximum possible. - Note that we cannot pick
5twice, even though it appears two times in the array.
If the number of distinct values in nums is less than k, you simply return all the distinct values in descending order.
How We Pick the Algorithm
Why Greedy Algorithms?
This problem maps to Greedy Algorithms through a short path in the full flowchart.
The problem greedily picks the largest distinct values from the array to maximize the sum, sorting and deduplicating to select up to k elements.
Open in FlowchartIntuition
The first observation is that every number in nums is positive. This means adding any element to our selection can only increase the total sum — it never hurts. So there is no reason to pick fewer elements than allowed: we should always try to pick exactly k elements (or all distinct values, if there are fewer than k of them).
The second observation comes from the distinctness constraint. Since each value can only be used once, duplicates in the array are useless beyond their first occurrence. We can mentally treat the array as a set of unique values.
Now the question becomes: given a set of distinct positive numbers, which k of them give the largest sum? The answer is clearly the k largest ones. This is a classic greedy choice — swapping any chosen number for a larger unchosen number would only increase the sum, so taking the biggest values first is always optimal.
How do we efficiently grab the largest distinct values? Sorting solves both problems at once:
- After sorting, the largest elements sit at the end of the array, so we can scan from right to left to pick big values first.
- Duplicates become adjacent after sorting, so detecting and skipping them is trivial: just compare each element with its neighbor
nums[i] == nums[i + 1]and skip if they match.
Finally, since we collect elements while scanning from largest to smallest and skip duplicates, the result we build is automatically in strictly descending order — exactly the format the problem asks for. No extra sorting or post-processing is needed.
In short: positivity → take as many as possible; distinctness → skip duplicates; maximize sum → greedily take the largest; sorting → makes both the greedy pick and duplicate skipping easy.
Solution Approach
Following the reference approach, the solution uses sorting combined with a reverse traversal and a greedy selection pattern.
Step 1: Sort the array.
nums.sort()
n = len(nums)
Sorting nums in ascending order accomplishes two things at once:
- The largest values are placed at the end of the array.
- Any duplicate values become adjacent to each other, making them easy to detect.
Step 2: Traverse from the end (largest to smallest).
for i in range(n - 1, -1, -1):
We iterate from index n - 1 down to 0. This guarantees we always consider bigger values before smaller ones, which matches the greedy strategy of maximizing the sum.
Step 3: Skip duplicates.
if i + 1 < n and nums[i] == nums[i + 1]: continue
Since the array is sorted, a duplicate of nums[i] (if any) sits right next to it at index i + 1. If nums[i] equals nums[i + 1], the same value was already considered in a previous iteration, so we skip it. The check i + 1 < n simply guards against going out of bounds for the last element.
A subtle detail: this skipping rule effectively keeps only the occurrence at the smallest index of each duplicate group, but since all duplicates have the same value, which occurrence we keep does not matter.
Step 4: Greedily collect the element and count down k.
ans.append(nums[i]) k -= 1 if k == 0: break
Every distinct value we encounter is appended to ans. Because we traverse from largest to smallest and never append the same value twice, ans is built in strictly descending order automatically — no extra work needed. Once we have collected k elements, we stop early with break.
If the loop finishes before k reaches 0, it means the array has fewer than k distinct values, and ans correctly contains all of them.
Step 5: Return the result.
return ans
Complexity Analysis
- Time complexity:
O(n log n), dominated by the sort. The reverse scan afterward is a single pass costingO(n). - Space complexity:
O(log n)for the sorting recursion stack (ignoring the output arrayans).
Why this works
The correctness rests on three facts working together:
- Greedy validity — with all-positive values, the
klargest distinct values always yield the maximum sum. - Sorting as a tool — it orders values for the greedy pick and clusters duplicates for
O(1)skip checks, avoiding the need for an extra hash set. - Free ordering — scanning from right to left produces the required strictly descending output as a natural by-product.
An alternative implementation could deduplicate first with a hash set, sort the unique values in descending order, and take the first k — same O(n log n) time, but the reference approach achieves it with a single sort and no auxiliary set.
Example Walkthrough
Let's trace the algorithm with nums = [4, 7, 4, 2, 9] and k = 3.
Step 1: Sort the array.
nums = [2, 4, 4, 7, 9] n = 5
After sorting, the largest values sit at the end, and the duplicate 4s are now adjacent (indices 1 and 2) — exactly what we need for easy skipping.
Step 2–4: Traverse from right to left, skipping duplicates and collecting values.
| Iteration | i | nums[i] | Duplicate check nums[i] == nums[i+1]? | Action | ans | k remaining |
|---|---|---|---|---|---|---|
| 1 | 4 | 9 | i + 1 = 5 is out of bounds → no check | Take it | [9] | 2 |
| 2 | 3 | 7 | 7 == 9? No | Take it | [9, 7] | 1 |
| 3 | 2 | 4 | 4 == 7? No | Take it | [9, 7, 4] | 0 → break |
Detailed reasoning per iteration:
i = 4(value 9): This is the last element, so there is no right neighbor to compare against. It's the largest value overall — greedily take it.ans = [9],kdrops to 2.i = 3(value 7): Its right neighbor is9, not equal, so7is a new distinct value. Take it.ans = [9, 7],kdrops to 1.i = 2(value 4): Its right neighbor is7, not equal, so this is the first time we see4during the scan. Take it.ans = [9, 7, 4],khits 0, and webreakimmediately.
Notice that the loop stops before ever reaching i = 1 (the second 4) or i = 0 (the 2). Had k been larger — say k = 4 — the iteration at i = 1 would find nums[1] == nums[2] (both 4) and continue, correctly skipping the duplicate, then pick up 2 at i = 0, yielding [9, 7, 4, 2].
Step 5: Return the result.
ans = [9, 7, 4]
The sum is 9 + 7 + 4 = 20, the maximum achievable with 3 distinct values, and the output is already in strictly descending order — no post-processing required.
Edge case check (fewer distinct values than k): with nums = [3, 3] and k = 5, sorting gives [3, 3]. The scan takes 3 at i = 1, then skips i = 0 as a duplicate. The loop ends with k = 4 still remaining, and we correctly return [3] — all available distinct values.
Solution Implementation
1class Solution:
2 def maxKDistinct(self, nums: List[int], k: int) -> List[int]:
3 # Sort the array in ascending order so that duplicates become adjacent
4 # and larger values are gathered at the end.
5 nums.sort()
6 n = len(nums)
7 answer = []
8
9 # Traverse from the largest element (rightmost) to the smallest (leftmost).
10 for i in range(n - 1, -1, -1):
11 # Skip the current element if it equals the one to its right,
12 # ensuring each value is taken only once (distinct elements only).
13 if i + 1 < n and nums[i] == nums[i + 1]:
14 continue
15
16 # Pick the current distinct element as one of the maximum values.
17 answer.append(nums[i])
18 k -= 1
19
20 # Stop once k distinct maximum elements have been collected.
21 if k == 0:
22 break
23
24 return answer
251class Solution {
2 public int[] maxKDistinct(int[] nums, int k) {
3 // Sort the array in ascending order so that the largest
4 // elements are located at the end of the array
5 Arrays.sort(nums);
6
7 int n = nums.length;
8 List<Integer> result = new ArrayList<>();
9
10 // Traverse the sorted array from right to left,
11 // i.e., from the largest element to the smallest
12 for (int i = n - 1; i >= 0; --i) {
13 // Skip duplicate elements: if the current element equals
14 // the one to its right, it has already been considered
15 if (i + 1 < n && nums[i] == nums[i + 1]) {
16 continue;
17 }
18
19 // Collect the current distinct element
20 result.add(nums[i]);
21
22 // Stop once we have gathered k distinct elements
23 if (--k == 0) {
24 break;
25 }
26 }
27
28 // Convert the list of integers into an int array and return it
29 return result.stream().mapToInt(Integer::intValue).toArray();
30 }
31}
321class Solution {
2public:
3 vector<int> maxKDistinct(vector<int>& nums, int k) {
4 // Sort the array in ascending order so that duplicates become adjacent
5 // and the largest elements are located at the end.
6 ranges::sort(nums);
7
8 int n = nums.size();
9 vector<int> ans;
10
11 // Traverse from the largest element (rightmost) to the smallest (leftmost).
12 for (int i = n - 1; i >= 0; --i) {
13 // Skip duplicates: if the current element equals the one to its right,
14 // it has already been considered, so move on.
15 if (i + 1 < n && nums[i] == nums[i + 1]) {
16 continue;
17 }
18
19 // Take the current distinct element (largest available so far).
20 ans.push_back(nums[i]);
21
22 // Stop once k distinct elements have been collected.
23 if (--k == 0) {
24 break;
25 }
26 }
27
28 return ans;
29 }
30};
311/**
2 * Returns the k largest distinct values from the input array,
3 * sorted in descending order.
4 *
5 * Approach:
6 * 1. Sort the array in ascending order so that duplicates become adjacent.
7 * 2. Traverse from the end (largest values first).
8 * 3. Skip any element that equals its right neighbor (duplicate).
9 * 4. Collect distinct values until k of them have been gathered.
10 *
11 * @param nums - The input array of numbers
12 * @param k - The number of distinct maximum values to collect
13 * @returns An array containing up to k largest distinct values in descending order
14 */
15function maxKDistinct(nums: number[], k: number): number[] {
16 // Sort the array in ascending order to group duplicates together
17 nums.sort((a, b) => a - b);
18
19 const ans: number[] = [];
20 const n: number = nums.length;
21
22 // Iterate from the largest element (rightmost) to the smallest
23 for (let i = n - 1; i >= 0; --i) {
24 // Skip duplicates: if the current element equals the one to its right,
25 // it has already been considered
26 if (i + 1 < n && nums[i] === nums[i + 1]) {
27 continue;
28 }
29
30 // Record the current distinct value
31 ans.push(nums[i]);
32
33 // Stop once k distinct values have been collected
34 if (--k === 0) {
35 break;
36 }
37 }
38
39 return ans;
40}
41Time and Space Complexity
-
Time Complexity:
O(n × log n), wherenis the length of thenumsarray.- Sorting the array with
nums.sort()takesO(n × log n)time, which dominates the overall cost. - The subsequent loop traverses the array from right to left at most once, contributing
O(n)time. - Therefore, the total time complexity is
O(n × log n) + O(n) = O(n × log n).
- Sorting the array with
-
Space Complexity:
O(log n), ignoring the space used for the answer listans.- Python's built-in sort (Timsort) requires
O(log n)auxiliary space for its recursion/stack during sorting. - The loop itself only uses a constant number of extra variables (
i,k), contributingO(1)space. - Hence, the dominant auxiliary space is
O(log n).
- Python's built-in sort (Timsort) requires
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Writing the duplicate check in the wrong direction
When traversing from right to left, a frequent mistake is to compare the current element with the one on its left instead of its right:
# WRONG: compares with the left neighbor while scanning right-to-left if i - 1 >= 0 and nums[i] == nums[i - 1]: continue
This skips the first occurrence you encounter (the rightmost one in a duplicate group) and instead keeps a later iteration's occurrence — but if k runs out between those two iterations, you can lose a value entirely. Worse, combined with an early break, the interplay becomes hard to reason about. With sorted input and a right-to-left scan, the duplicate you have already seen sits at index i + 1, so the correct guard is:
# CORRECT if i + 1 < n and nums[i] == nums[i + 1]: continue
A simple way to remember it: always compare against the side you came from. Scanning right-to-left, you came from i + 1.
Pitfall 2: Decrementing k before skipping duplicates
Another subtle bug is counting an element toward k before verifying it is distinct, or structuring the loop so duplicates consume the budget:
# WRONG: k is consumed even for skipped duplicates
for i in range(n - 1, -1, -1):
k -= 1
if i + 1 < n and nums[i] == nums[i + 1]:
continue
answer.append(nums[i])
if k == 0:
break
With nums = [5, 3, 5, 8] and k = 3, this returns only [8, 5] because the duplicate 5 silently eats one unit of k. The fix is to decrement k only after an element has actually been appended, exactly as the reference code does.
Pitfall 3: Forgetting that fewer than k distinct values may exist
Some implementations assume the answer always has length k — for example, pre-allocating a result array of size k, or asserting len(answer) == k afterward:
# WRONG: crashes or leaves garbage when distinct count < k answer = [0] * k
If nums = [2, 2, 2] and k = 3, there is only one distinct value, and the correct output is [2]. The loop-with-break structure handles this naturally: when the scan finishes before k hits zero, answer simply holds all distinct values. Avoid any logic that hard-codes the output length to k.
Pitfall 4: Mutating the input without realizing it
nums.sort() sorts the list in place, permanently reordering the caller's data. On LeetCode this is harmless, but in real codebases (or in problems where the original order matters later) it can cause hidden side effects. If preserving the input matters, sort a copy instead:
sorted_nums = sorted(nums) # leaves the original untouched
Pitfall 5: Deduplicating with set() and expecting order for free
A tempting one-liner alternative:
# WRONG: set order is arbitrary — output may not be descending
return list(set(nums))[:k]
Python sets do not maintain sorted order, so slicing the first k elements gives neither the largest values nor a descending sequence. If you prefer the set-based approach, you must explicitly sort:
# CORRECT alternative
return sorted(set(nums), reverse=True)[:k]
Both this and the reference approach run in O(n log n); the reference version just avoids the auxiliary set by exploiting adjacency of duplicates after sorting.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhich of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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!