Facebook Pixel

3763. Maximum Total Sum with Threshold Constraints 🔒

MediumGreedyArraySortingHeap (Priority Queue)
LeetCode ↗

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 i such that threshold[i] <= step.
    • If no such index exists, the process ends.
  • Add nums[i] to your running total.
  • Mark index i as used and increment step by 1.

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.

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

How We Pick the Algorithm

Why Heap / Sortings?

This problem maps to Heap / Sortings through a short path in the full flowchart.

kthsmallest/largest?yesGreedyselection?yesHeap / Sortings

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 Flowchart

Intuition

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 < n and threshold[idx[i]] <= step, we push nums[idx[i]] into the sorted list sl and advance i. This guarantees that every number qualifying for the current step is in the structure before we make a choice.
  • Check termination: if sl is empty after this, there is no unused index satisfying the condition, so we break out of the loop.
  • Greedy pick: otherwise, we call sl.pop(), which removes and returns the largest value in the sorted list, and add it to ans. We then increment step by 1.

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 costs O(n × log n), and each of the n numbers is added to and removed from the sorted list at most once, with each operation costing O(log n).
  • Space complexity: O(n), for the index array idx and the sorted list sl.

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 ithreshold[i]nums[i]
023
111
235
312

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 ✓ → push nums[1] = 1. Now sl = [1], i = 1.
    • idx[1] = 3, threshold[3] = 1 <= 1 ✓ → push nums[3] = 2. Now sl = [1, 2], i = 2.
    • idx[2] = 0, threshold[0] = 2 <= 1? No. Stop adding.
  • Termination check: sl is not empty.
  • Greedy pick: sl.pop() returns the largest value 2. 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 ✓ → push nums[0] = 3. Now sl = [1, 3], i = 3.
    • idx[3] = 2, threshold[2] = 3 <= 2? No. Stop adding.
  • Termination check: sl is not empty.
  • Greedy pick: sl.pop() returns 3. 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 ✓ → push nums[2] = 5. Now sl = [1, 5], i = 4.
    • i = 4 reaches n, stop adding.
  • Termination check: sl is not empty.
  • Greedy pick: sl.pop() returns 5. 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() returns 1. ans = 10 + 1 = 11. sl = []. step = 5.

Iteration 5 (step = 5):

  • Add available numbers: i = 4 = n, nothing to add.
  • Termination check: sl is 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
75
1class 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}
45
1class 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};
44
1// 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}
78

Time 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 all n indices, which takes O(n × log n) time.

  • The main while True loop: To understand its cost, we need to examine the operations on the SortedList:

    • Add operations: The inner while loop with sl.add(...) runs in total at most n times across the entire execution (since i only increases and never exceeds n). Each sl.add(...) on a SortedList costs O(log n), contributing O(n × log n) overall.
    • Pop operations: Each iteration of the outer loop performs one sl.pop(), which costs O(log n). Since each pop removes one element that was previously added, the total number of pops is bounded by n. This contributes O(n × log n) overall.
    • The variable step increases by 1 each iteration, and the loop terminates when sl becomes empty, ensuring the outer loop runs at most O(n) times.

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 idx list stores all n indices, requiring O(n) space.
  • The SortedList sl holds at most n elements at any time, requiring O(n) space.
  • Other variables (step, ans, i, n) use O(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 is threshold[i] <= step. Writing threshold[sorted_indices[next_index]] < step incorrectly delays (or permanently skips) indices whose threshold equals the current step, producing a smaller-than-optimal sum.
  • Forgetting that step keeps growing even when you skip "early". Because step only advances when you pick a value, you might assume an index with a high threshold can never be reached. In fact, every successful pick raises step, which may unlock previously locked indices. The inner while loop 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 in SortedList) instead of sl.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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More