3763. Maximum Total Sum with Threshold Constraints 🔒
Problem Description
You are given two integer arrays nums and threshold, both of length n.
Starting at step = 1, you repeatedly perform the following operations:
- Choose an unused index
isuch thatthreshold[i] <= step.- If no such index exists, the process ends.
- Add
nums[i]to your running total. - Mark index
ias used and incrementstepby1.
In other words, at every step you may pick any index that has not been used yet, as long as its threshold value does not exceed the current step number. Once an index is picked, it cannot be chosen again, and the step counter advances by one. The process keeps going until there is no valid index left to choose.
Your task is to return the maximum total sum you can obtain by choosing indices optimally.
How We Pick the Algorithm
Why Heap / Sortings?
This problem maps to Heap / Sortings through a short path in the full flowchart.
A max-heap paired with greedy selection at each step (take the largest available number) maximizes the total sum as the step counter increases.
Open in FlowchartIntuition
The key observation is that at each step, the step counter only ever increases. This means once an index becomes "available" (its threshold[i] <= step), it stays available for all future steps as well. So as step grows from 1 upward, the pool of usable indices can only get larger, never smaller.
Now think about what we should do at each individual step. Among all currently available indices, which one should we pick? Since each pick adds nums[i] to our total and consumes exactly one step, and since picking does not affect which indices are available later (availability only depends on threshold[i] <= step, not on what we picked before), we should simply grab the largest available number at every step. There is no downside to taking the biggest one now — the rest remain available later anyway. This is the classic greedy insight: a locally optimal choice (take the maximum available) leads to a globally optimal result.
To carry out this greedy strategy efficiently, we process indices in order of increasing threshold. We sort an index array idx by threshold ascending. As step increases, we keep adding every index whose threshold is now <= step into a structure that lets us quickly retrieve the maximum — such as a max heap or a sorted list. At each step we pop the largest value and add it to our running total, then advance step by one.
The process naturally terminates when the structure is empty after we have added all currently qualifying numbers. An empty structure means there is no unused index satisfying threshold[i] <= step, which is exactly the stopping condition described in the problem.
Pattern Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.
Solution Approach
Solution 1: Greedy + Sorting
Based on the intuition above, we want to always pick the largest available number at each step. To do this efficiently, we combine sorting with a sorted list (or max heap).
We first build an index array idx of length n and sort it in ascending order by the corresponding threshold values, using sorted(range(n), key=lambda i: threshold[i]). This lets us bring indices "online" in the exact order their thresholds get satisfied as step grows.
We then maintain a SortedList named sl to hold all the nums values that are currently usable (i.e., whose threshold is <= step). We use a pointer i to track how far we have advanced through the sorted index array, a variable step starting at 1, and an accumulator ans for the total sum.
The main loop works as follows:
- Add newly available numbers: while
i < nandthreshold[idx[i]] <= step, we pushnums[idx[i]]into the sorted listsland advancei. This guarantees that every number qualifying for the current step is in the structure before we make a choice. - Check termination: if
slis empty after this, there is no unused index satisfying the condition, so webreakout of the loop. - Greedy pick: otherwise, we call
sl.pop(), which removes and returns the largest value in the sorted list, and add it toans. We then incrementstepby1.
Because step keeps increasing, indices that were not yet available may become available in later iterations, and the inner while loop handles adding them at the right time. The loop ends exactly when no further valid choice exists.
Finally, we return ans as the maximum total sum.
Complexity Analysis:
- Time complexity:
O(n × log n). Sorting the index array costsO(n × log n), and each of thennumbers is added to and removed from the sorted list at most once, with each operation costingO(log n). - Space complexity:
O(n), for the index arrayidxand the sorted listsl.
Example Walkthrough
Let's trace through the solution approach with a small example.
Input:
nums = [3, 1, 5, 2]threshold = [2, 1, 3, 1]n = 4
Step 0: Sort indices by threshold
We build idx = sorted(range(4), key=lambda i: threshold[i]).
Looking at the threshold values paired with their indices:
index i | threshold[i] | nums[i] |
|---|---|---|
| 0 | 2 | 3 |
| 1 | 1 | 1 |
| 2 | 3 | 5 |
| 3 | 1 | 2 |
Sorting by threshold ascending gives idx = [1, 3, 0, 2] (thresholds 1, 1, 2, 3).
Now we initialize: i = 0, step = 1, ans = 0, sl = [].
Iteration 1 (step = 1):
- Add available numbers: check
threshold[idx[i]] <= 1.idx[0] = 1,threshold[1] = 1 <= 1✓ → pushnums[1] = 1. Nowsl = [1],i = 1.idx[1] = 3,threshold[3] = 1 <= 1✓ → pushnums[3] = 2. Nowsl = [1, 2],i = 2.idx[2] = 0,threshold[0] = 2 <= 1? No. Stop adding.
- Termination check:
slis not empty. - Greedy pick:
sl.pop()returns the largest value2.ans = 0 + 2 = 2.sl = [1].step = 2.
Iteration 2 (step = 2):
- Add available numbers: check
threshold[idx[i]] <= 2.idx[2] = 0,threshold[0] = 2 <= 2✓ → pushnums[0] = 3. Nowsl = [1, 3],i = 3.idx[3] = 2,threshold[2] = 3 <= 2? No. Stop adding.
- Termination check:
slis not empty. - Greedy pick:
sl.pop()returns3.ans = 2 + 3 = 5.sl = [1].step = 3.
Iteration 3 (step = 3):
- Add available numbers: check
threshold[idx[i]] <= 3.idx[3] = 2,threshold[2] = 3 <= 3✓ → pushnums[2] = 5. Nowsl = [1, 5],i = 4.i = 4reachesn, stop adding.
- Termination check:
slis not empty. - Greedy pick:
sl.pop()returns5.ans = 5 + 5 = 10.sl = [1].step = 4.
Iteration 4 (step = 4):
- Add available numbers:
i = 4 = n, nothing to add. - Termination check:
sl = [1]is not empty. - Greedy pick:
sl.pop()returns1.ans = 10 + 1 = 11.sl = [].step = 5.
Iteration 5 (step = 5):
- Add available numbers:
i = 4 = n, nothing to add. - Termination check:
slis empty → break.
Result: ans = 11.
Why this is optimal: Notice that even though index 2 (value 5) had the highest threshold and only became available at step = 3, the greedy strategy still captured all four numbers 1 + 2 + 3 + 5 = 11. By always popping the largest available value, we never block ourselves: every number stays available once its threshold is met, so taking the maximum each step costs us nothing in future opportunities.
Solution Implementation
1from typing import List
2from sortedcontainers import SortedList
3
4
5class Solution:
6 def maxSum(self, nums: List[int], threshold: List[int]) -> int:
7 n = len(nums)
8
9 # Sort indices in ascending order of their threshold values,
10 # so we can unlock elements progressively as 'step' increases.
11 sorted_indices = sorted(range(n), key=lambda i: threshold[i])
12
13 # Maintains currently available (unlocked) values in sorted order,
14 # allowing us to efficiently pop the maximum.
15 available_values = SortedList()
16
17 step = 1 # Current threshold level being processed.
18 total = 0 # Accumulated maximum sum (the answer).
19 next_index = 0 # Pointer into sorted_indices for unlocking.
20
21 while True:
22 # Unlock every element whose threshold is now satisfied (<= step).
23 while next_index < n and threshold[sorted_indices[next_index]] <= step:
24 available_values.add(nums[sorted_indices[next_index]])
25 next_index += 1
26
27 # If nothing is available to pick, we are done.
28 if not available_values:
29 break
30
31 # Greedily take the largest currently available value.
32 total += available_values.pop()
33
34 # Move on to the next threshold level.
35 step += 1
36
37 return total
38```
39
40## Alternative Perspective
41
42If you want to **avoid the external `sortedcontainers` dependency**, you can use Python's built-in `heapq` as a max-heap (by negating values):
43
44```python3
45from typing import List
46import heapq
47
48
49class Solution:
50 def maxSum(self, nums: List[int], threshold: List[int]) -> int:
51 n = len(nums)
52
53 # Sort indices by ascending threshold for progressive unlocking.
54 sorted_indices = sorted(range(n), key=lambda i: threshold[i])
55
56 max_heap = [] # Max-heap simulated via negated values.
57 step = 1 # Current threshold level.
58 total = 0 # Accumulated answer.
59 next_index = 0 # Pointer for unlocking elements.
60
61 while True:
62 # Unlock all elements whose threshold <= current step.
63 while next_index < n and threshold[sorted_indices[next_index]] <= step:
64 heapq.heappush(max_heap, -nums[sorted_indices[next_index]])
65 next_index += 1
66
67 if not max_heap:
68 break
69
70 # Pop the largest available value (negate back to positive).
71 total += -heapq.heappop(max_heap)
72 step += 1
73
74 return total
751class Solution {
2 public long maxSum(int[] nums, int[] threshold) {
3 int n = nums.length;
4
5 // Create an index array to sort element indices by their threshold values
6 Integer[] indices = new Integer[n];
7 Arrays.setAll(indices, i -> i);
8
9 // Sort indices in ascending order based on the corresponding threshold value
10 Arrays.sort(indices, Comparator.comparingInt(i -> threshold[i]));
11
12 // TreeMap acts as a multiset: key = element value, value = count of that element
13 // It keeps elements sorted so we can quickly access the largest available value
14 TreeMap<Integer, Integer> availableValues = new TreeMap<>();
15
16 long answer = 0;
17
18 // Iterate step by step; at each step we "unlock" elements whose threshold is reached,
19 // then greedily pick the largest available value
20 for (int i = 0, step = 1; ; ++step) {
21 // Add all elements whose threshold is satisfied at the current step
22 while (i < n && threshold[indices[i]] <= step) {
23 availableValues.merge(nums[indices[i]], 1, Integer::sum);
24 ++i;
25 }
26
27 // If no elements are available, we are done
28 if (availableValues.isEmpty()) {
29 break;
30 }
31
32 // Greedily take the largest available value to maximize the sum
33 int maxValue = availableValues.lastKey();
34 answer += maxValue;
35
36 // Decrement the count of the chosen value; remove it entirely if count drops to 0
37 if (availableValues.merge(maxValue, -1, Integer::sum) == 0) {
38 availableValues.remove(maxValue);
39 }
40 }
41
42 return answer;
43 }
44}
451class Solution {
2public:
3 long long maxSum(vector<int>& nums, vector<int>& threshold) {
4 int n = nums.size();
5
6 // Build an index array [0, 1, ..., n-1] so we can sort by threshold
7 // without disturbing the original nums/threshold arrays.
8 vector<int> order(n);
9 iota(order.begin(), order.end(), 0);
10
11 // Sort indices in ascending order of their threshold value.
12 // Elements with smaller thresholds become available earlier.
13 sort(order.begin(), order.end(),
14 [&](int lhs, int rhs) { return threshold[lhs] < threshold[rhs]; });
15
16 // multiset keeps all currently available values, ordered ascending,
17 // so the largest available value is always at the end.
18 multiset<int> available;
19 long long answer = 0;
20 int nextIdx = 0; // pointer into the sorted "order" array
21
22 // Iterate step by step (step = 1, 2, 3, ...).
23 for (int step = 1;; ++step) {
24 // Release every element whose threshold has been reached at this step.
25 while (nextIdx < n && threshold[order[nextIdx]] <= step) {
26 available.insert(nums[order[nextIdx]]);
27 ++nextIdx;
28 }
29
30 // No element is available to pick: stop processing.
31 if (available.empty()) {
32 break;
33 }
34
35 // Greedily take the largest available value for this step.
36 auto largest = prev(available.end());
37 answer += *largest;
38 available.erase(largest);
39 }
40
41 return answer;
42 }
43};
441// Max-heap stored as a global array; the largest value sits at index 0.
2const heap: number[] = [];
3
4// Insert a value into the max-heap, restoring the heap property by sifting up.
5function heapPush(value: number): void {
6 heap.push(value);
7 let child = heap.length - 1;
8 while (child > 0) {
9 const parent = (child - 1) >> 1;
10 if (heap[parent] >= heap[child]) {
11 break;
12 }
13 [heap[parent], heap[child]] = [heap[child], heap[parent]];
14 child = parent;
15 }
16}
17
18// Remove and return the maximum value, restoring the heap property by sifting down.
19function heapPop(): number {
20 const top = heap[0];
21 const last = heap.pop()!;
22 if (heap.length > 0) {
23 heap[0] = last;
24 let parent = 0;
25 const size = heap.length;
26 while (true) {
27 const left = parent * 2 + 1;
28 const right = parent * 2 + 2;
29 let largest = parent;
30 if (left < size && heap[left] > heap[largest]) {
31 largest = left;
32 }
33 if (right < size && heap[right] > heap[largest]) {
34 largest = right;
35 }
36 if (largest === parent) {
37 break;
38 }
39 [heap[parent], heap[largest]] = [heap[largest], heap[parent]];
40 parent = largest;
41 }
42 }
43 return top;
44}
45
46function maxSum(nums: number[], threshold: number[]): number {
47 const length = nums.length;
48
49 // Reset the global heap so repeated calls start clean.
50 heap.length = 0;
51
52 // Build a list of indices sorted by their threshold value in ascending order.
53 const sortedIndices = Array.from({ length }, (_, i) => i).sort(
54 (a, b) => threshold[a] - threshold[b],
55 );
56
57 let answer = 0;
58
59 // pointer scans sortedIndices; step represents the current "time" level.
60 for (let pointer = 0, step = 1; ; ++step) {
61 // Enqueue every value whose threshold is now reachable at this step.
62 while (pointer < length && threshold[sortedIndices[pointer]] <= step) {
63 heapPush(nums[sortedIndices[pointer]]);
64 ++pointer;
65 }
66
67 // No eligible value remains, so we are finished.
68 if (heap.length === 0) {
69 break;
70 }
71
72 // Greedily take the largest currently available value.
73 answer += heapPop();
74 }
75
76 return answer;
77}
78Time and Space Complexity
Time Complexity
The time complexity is O(n × log n), where n is the length of the array nums.
The analysis is as follows:
-
Sorting indices: The line
idx = sorted(range(n), key=lambda i: threshold[i])sorts allnindices, which takesO(n × log n)time. -
The main
while Trueloop: To understand its cost, we need to examine the operations on theSortedList:- Add operations: The inner
whileloop withsl.add(...)runs in total at mostntimes across the entire execution (sinceionly increases and never exceedsn). Eachsl.add(...)on aSortedListcostsO(log n), contributingO(n × log n)overall. - Pop operations: Each iteration of the outer loop performs one
sl.pop(), which costsO(log n). Since eachpopremoves one element that was previously added, the total number of pops is bounded byn. This contributesO(n × log n)overall. - The variable
stepincreases by 1 each iteration, and the loop terminates whenslbecomes empty, ensuring the outer loop runs at mostO(n)times.
- Add operations: The inner
Combining these, the dominant cost is O(n × log n).
Space Complexity
The space complexity is O(n), where n is the length of the array nums.
The analysis is as follows:
- The
idxlist stores allnindices, requiringO(n)space. - The
SortedListslholds at mostnelements at any time, requiringO(n)space. - Other variables (
step,ans,i,n) useO(1)space.
Therefore, the total space complexity is O(n).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Re-checking already "online" indices on every step (TLE)
A naive instinct is, on each step, to scan the entire array looking for unused indices whose threshold <= step, then pick the maximum among them:
# WRONG: O(n^2) — re-scans all indices every step
used = [False] * n
step = 1
total = 0
while True:
best, best_i = -1, -1
for i in range(n):
if not used[i] and threshold[i] <= step and nums[i] > best:
best, best_i = nums[i], i
if best_i == -1:
break
total += best
used[best_i] = True
step += 1
This works logically but runs in O(n^2), which times out for large n. The fix is the intended approach: sort indices by threshold and use a pointer so each index is unlocked exactly once, while a heap/sorted list answers the "current maximum" query in O(log n).
Pitfall 2: Mishandling the relationship between step and unlocking
Two subtle mistakes occur around the unlock condition:
- Using
<instead of<=. The rule isthreshold[i] <= step. Writingthreshold[sorted_indices[next_index]] < stepincorrectly delays (or permanently skips) indices whose threshold equals the current step, producing a smaller-than-optimal sum. - Forgetting that
stepkeeps growing even when you skip "early". Becausesteponly advances when you pick a value, you might assume an index with a high threshold can never be reached. In fact, every successful pick raisesstep, which may unlock previously locked indices. The innerwhileloop must run every iteration (not just once) so newly qualifying indices come online at the right moment.
Pitfall 3: Breaking the loop too early
The termination check must come after the unlock loop, not before:
# WRONG: checks emptiness before unlocking new elements while True: if not available_values: # may break prematurely! break while next_index < n and threshold[...] <= step: available_values.add(...) next_index += 1 ...
At step = 1, available_values is empty, so this version breaks immediately and returns 0, even when valid indices exist. Always unlock first, then test for emptiness, then pick.
Pitfall 4: Picking the minimum instead of the maximum
The greedy goal is to grab the largest available value at each step. Common slips:
- Calling
sl.pop(0)(smallest inSortedList) instead ofsl.pop()(largest). - Forgetting to negate when using
heapq, which is a min-heap by default. Push-nums[i]and negate again on pop; otherwise you accumulate the smallest values and get a wrong (smaller) total.
Pitfall 5: Assuming all indices get used
It is tempting to think the process consumes every index, but if some index has a threshold larger than the maximum reachable step, it can never be picked. Since step increases by exactly 1 per pick, the highest step you can reach equals 1 + (number of picks made). Any index whose threshold exceeds that ceiling stays locked forever — the loop correctly handles this by terminating when nothing new unlocks and the structure is empty, so do not force-add leftover indices to the total.
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 following uses divide and conquer strategy?
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
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
Want a Structured Path to Master System Design Too? Don’t Miss This!