Facebook Pixel

3767. Maximize Points After Choosing K Tasks

MediumGreedyArraySortingHeap (Priority Queue)
LeetCode ↗

Problem Description

You are given two integer arrays, technique1 and technique2, each of length n, where n represents the number of tasks you need to complete.

For each task i, you have two ways to complete it:

  • If you complete the ith task using technique 1, you earn technique1[i] points.
  • If you complete the ith task using technique 2, you earn technique2[i] points.

You are also given an integer k, which represents the minimum number of tasks that must be completed using technique 1.

The rules are:

  • You must complete at least k tasks using technique 1. These do not have to be the first k tasks — they can be any k tasks of your choosing.
  • The remaining tasks can be completed using either technique, whichever you prefer.

Your goal is to maximize the total number of points earned across all n tasks.

Return an integer representing the maximum total points you can earn.

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/largestoryesGreedywithpriority?yesHeap / Sortings

Selecting which tasks to switch to technique 1 to maximize points involves sorting gains and using a heap for the best k selections.

Open in Flowchart

Intuition

Let's start by thinking about a simpler baseline. Suppose we ignore the constraint for a moment and just assign every task to technique 2. This gives us a starting total score of sum(technique2).

Now, the question becomes: which tasks should we switch from technique 2 to technique 1?

For each task i, switching it to technique 1 changes our score by technique1[i] - technique2[i]. Let's call this value the gain of task i (it can be positive, negative, or zero). If we switch task i, our total score changes by exactly this amount.

With this view, the problem splits into two parts:

  1. The forced part: We are required to use technique 1 on at least k tasks. Since switching a task affects the score by its gain, and we have no choice but to switch k of them, we should pick the k tasks with the largest gains. This way, the unavoidable switches hurt us as little as possible (or help us as much as possible). To do this, we sort all tasks by their gain technique1[i] - technique2[i] in descending order and pick the top k.

  2. The optional part: For all remaining tasks, switching is purely optional. Here the rule is simple — switch a task to technique 1 only if it increases (or keeps) the score, meaning we switch when technique1[i] >= technique2[i] (i.e., the gain is non-negative). Switching a task with a negative gain would only lower our total, so we leave those on technique 2.

This greedy choice works because each task's decision is independent — switching one task never affects the gain of another. So picking the largest gains for the forced k and then greedily taking every remaining non-negative gain guarantees the maximum possible total.

Pattern Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.

Solution Approach

We use a Greedy + Sorting strategy. Let n be the number of tasks.

Step 1: Start with everything on technique 2.

We initialize our answer as the sum of all technique 2 values:

ans = sum(technique2)

This represents the score if no task uses technique 1 yet.

Step 2: Sort tasks by their gain in descending order.

For each task i, the gain of switching to technique 1 is technique1[i] - technique2[i]. We sort the task indices (not the values themselves) by this gain from largest to smallest:

idx = sorted(range(n), key=lambda i: -(technique1[i] - technique2[i]))

Here idx is an ordered list of task indices. The first elements of idx are the tasks that benefit the most (or lose the least) from switching to technique 1. Using indices lets us look up both technique1[i] and technique2[i] later.

Step 3: Handle the forced k tasks.

We must use technique 1 on at least k tasks. To minimize the cost (or maximize the benefit) of this requirement, we pick the first k entries of idx — the ones with the largest gains. For each, we remove its technique 2 contribution and add its technique 1 contribution:

for i in idx[:k]:
    ans -= technique2[i]
    ans += technique1[i]

This is equivalent to adding each task's gain to ans, and we do it for the k best gains.

Step 4: Handle the remaining optional tasks.

For every remaining task (idx[k:]), switching is optional. We only switch when it does not reduce the score, i.e., when technique1[i] >= technique2[i]:

for i in idx[k:]:
    if technique1[i] >= technique2[i]:
        ans -= technique2[i]
        ans += technique1[i]

Tasks with a negative gain are left on technique 2, so they keep their original contribution.

Step 5: Return the result.

After both loops, ans holds the maximum achievable score:

return ans

Complexity Analysis:

  • Time complexity: O(n log n), dominated by the sorting step. The two loops together run in O(n).
  • Space complexity: O(n), for storing the sorted index list idx.

Example Walkthrough

Let's trace through the solution with a small, concrete example.

Input:

  • technique1 = [3, 1, 5, 2]
  • technique2 = [2, 4, 1, 2]
  • k = 2
  • n = 4

This means we have 4 tasks, and we must use technique 1 on at least 2 of them.


Step 1: Start with everything on technique 2.

We assume every task uses technique 2 and sum those values:

ans = sum(technique2) = 2 + 4 + 1 + 2 = 9

So our baseline score is 9.


Step 2: Compute each task's gain and sort by gain (descending).

The gain of switching task i to technique 1 is technique1[i] - technique2[i]:

Task itechnique1[i]technique2[i]Gain
032+1
114-3
251+4
3220

Sorting the task indices by gain in descending order:

Gains:  +4 (task 2),  +1 (task 0),  0 (task 3),  -3 (task 1)
idx  = [2, 0, 3, 1]

Step 3: Handle the forced k = 2 tasks.

We must switch at least 2 tasks, so we take the first 2 entries of idx → tasks 2 and 0 (the largest gains). For each, remove its technique 2 contribution and add its technique 1 contribution:

  • Task 2: ans = 9 - technique2[2] + technique1[2] = 9 - 1 + 5 = 13 (gain +4)
  • Task 0: ans = 13 - technique2[0] + technique1[0] = 13 - 2 + 3 = 14 (gain +1)

After the forced switches, ans = 14.


Step 4: Handle the remaining optional tasks.

The remaining tasks are idx[2:] = [3, 1]. We switch each only if technique1[i] >= technique2[i]:

  • Task 3: technique1[3] = 2, technique2[3] = 22 >= 2 is true, so switch: ans = 14 - 2 + 2 = 14 (gain 0, no harm)
  • Task 1: technique1[1] = 1, technique2[1] = 41 >= 4 is false, so skip. Task 1 stays on technique 2.

After the optional switches, ans = 14.


Step 5: Return the result.

The maximum total points is:

ans = 14

Verification of the final assignment:

TaskTechnique usedPoints
0Technique 13
1Technique 24
2Technique 15
3Technique 1 (or 2)2
  • Tasks using technique 1: {0, 2, 3} → 3 tasks, which satisfies the constraint of at least k = 2. ✓
  • Total points: 3 + 4 + 5 + 2 = 14. ✓

The greedy choice locked in the two highest-gain tasks for the forced requirement, then freely grabbed every remaining non-negative gain, leaving only the genuinely losing switch (task 1) untouched — yielding the optimal total of 14.

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def maxPoints(self, technique1: List[int], technique2: List[int], k: int) -> int:
6        n = len(technique1)
7
8        # Sort indices by the gain (technique1 - technique2) in descending order.
9        # The largest gains come first, so the top k forced picks lose the least.
10        sorted_indices = sorted(
11            range(n),
12            key=lambda i: -(technique1[i] - technique2[i]),
13        )
14
15        # Baseline: assume we pick technique2 for every index.
16        total = sum(technique2)
17
18        # For the first k indices (highest gain), we are forced to use technique1.
19        # Swap out technique2 and swap in technique1 for each of them.
20        for i in sorted_indices[:k]:
21            total -= technique2[i]
22            total += technique1[i]
23
24        # For the remaining indices, switching is optional.
25        # Only switch to technique1 when it is at least as large as technique2.
26        for i in sorted_indices[k:]:
27            if technique1[i] >= technique2[i]:
28                total -= technique2[i]
29                total += technique1[i]
30
31        return total
32
1class Solution {
2    public long maxPoints(int[] technique1, int[] technique2, int k) {
3        int n = technique1.length;
4
5        // Create an index array so we can sort indices without disturbing
6        // the original input arrays.
7        Integer[] indices = new Integer[n];
8        Arrays.setAll(indices, i -> i);
9
10        // Sort indices in descending order by the "gain" of switching from
11        // technique2 to technique1, i.e. (technique1[i] - technique2[i]).
12        // The largest gains come first.
13        Arrays.sort(indices,
14                (a, b) -> (technique1[b] - technique2[b]) - (technique1[a] - technique2[a]));
15
16        // Start by assuming every element uses technique2.
17        long answer = 0;
18        for (int value : technique2) {
19            answer += value;
20        }
21
22        // For the first k indices (the ones with the largest gain) we are
23        // forced/allowed to switch to technique1, regardless of sign.
24        for (int i = 0; i < k; i++) {
25            int index = indices[i];
26            answer -= technique2[index];
27            answer += technique1[index];
28        }
29
30        // For the remaining indices, only switch to technique1 when it does
31        // not decrease the total (i.e. the gain is non-negative).
32        for (int i = k; i < n; i++) {
33            int index = indices[i];
34            if (technique1[index] >= technique2[index]) {
35                answer -= technique2[index];
36                answer += technique1[index];
37            }
38        }
39
40        return answer;
41    }
42}
43
1class Solution {
2public:
3    long long maxPoints(vector<int>& technique1, vector<int>& technique2, int k) {
4        int n = technique1.size();
5
6        // Create an index array [0, 1, 2, ..., n-1]
7        vector<int> indices(n);
8        iota(indices.begin(), indices.end(), 0);
9
10        // Sort indices by the gain (technique1[i] - technique2[i]) in descending order.
11        // The indices with the largest gain come first, so we can prioritize
12        // applying technique1 on them within the limit k.
13        sort(indices.begin(), indices.end(), [&](int i, int j) {
14            return (technique1[i] - technique2[i]) > (technique1[j] - technique2[j]);
15        });
16
17        // Start by assuming technique2 is applied to every element.
18        long long answer = 0;
19        for (int value : technique2) {
20            answer += value;
21        }
22
23        // For the first k elements (those with the highest gain), we are forced
24        // (or at least allowed) to switch from technique2 to technique1.
25        for (int i = 0; i < k; ++i) {
26            int index = indices[i];
27            answer -= technique2[index];   // Remove the technique2 contribution
28            answer += technique1[index];   // Add the technique1 contribution
29        }
30
31        // For the remaining elements, switch to technique1 only when it is
32        // beneficial (i.e., technique1 yields at least as much as technique2).
33        for (int i = k; i < n; ++i) {
34            int index = indices[i];
35            if (technique1[index] >= technique2[index]) {
36                answer -= technique2[index];   // Remove the technique2 contribution
37                answer += technique1[index];   // Add the technique1 contribution
38            }
39        }
40
41        return answer;
42    }
43};
44
1/**
2 * Calculates the maximum total points achievable.
3 *
4 * Each element can contribute either its value from `technique1` or `technique2`.
5 * We must use `technique1` for exactly at least `k` elements (the ones where the
6 * gain from switching to technique1 is largest). For the remaining elements,
7 * we freely pick whichever technique yields a higher value.
8 *
9 * @param technique1 - Point values when using the first technique.
10 * @param technique2 - Point values when using the second technique.
11 * @param k - The minimum number of elements that must use technique1.
12 * @returns The maximum total points obtainable.
13 */
14function maxPoints(technique1: number[], technique2: number[], k: number): number {
15    const length: number = technique1.length;
16
17    // Index array used to order elements by their "gain" of switching to technique1.
18    const indices: number[] = Array.from({ length }, (_, i) => i);
19
20    // Sort indices in descending order of (technique1 - technique2).
21    // The largest gains come first, so the first k indices are the best
22    // candidates to forcibly assign technique1.
23    indices.sort(
24        (i, j) =>
25            (technique1[j] - technique2[j]) - (technique1[i] - technique2[i]),
26    );
27
28    // Start by assuming every element uses technique2.
29    let answer: number = technique2.reduce((sum, value) => sum + value, 0);
30
31    // For the top k elements (largest gain), force the use of technique1
32    // regardless of whether it is beneficial.
33    for (let i = 0; i < k; i++) {
34        const index: number = indices[i];
35        answer -= technique2[index];
36        answer += technique1[index];
37    }
38
39    // For the remaining elements, switch to technique1 only when it gives
40    // a value greater than or equal to technique2.
41    for (let i = k; i < length; i++) {
42        const index: number = indices[i];
43        if (technique1[index] >= technique2[index]) {
44            answer -= technique2[index];
45            answer += technique1[index];
46        }
47    }
48
49    return answer;
50}
51

Time and Space Complexity

  • Time Complexity: O(n × log n), where n is the length of the input arrays. The dominant operation is sorted(range(n), key=...), which sorts the n indices and costs O(n × log n). The initial sum(technique2) takes O(n), and the two subsequent loops iterate over the k selected indices and the remaining n - k indices respectively, together contributing O(n). Therefore the overall time complexity is O(n × log n).

  • Space Complexity: O(n), where n is the length of the input arrays. The idx list created by sorted(range(n), ...) stores n indices, requiring O(n) extra space. The sorting algorithm itself may also use up to O(n) auxiliary space. Hence the overall space complexity is O(n).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Forgetting that the k forced tasks may have negative gain

The most common mistake is treating Step 4's optional logic as if it applied everywhere. A naive solution might be written as:

# WRONG: only switches when it helps, ignoring the "at least k" constraint
total = sum(technique2)
for i in range(n):
    if technique1[i] >= technique2[i]:
        total -= technique2[i]
        total += technique1[i]
return total

This silently ignores the requirement that at least k tasks must use technique 1. If fewer than k tasks have a non-negative gain, this code returns an invalid answer — it never forces the extra switches.

Why the correct solution avoids this: By sorting in descending order of gain and unconditionally switching the first k indices (even when their gain is negative), we guarantee the constraint is met while losing the least possible amount of points. The k smallest losses are exactly the tasks ranked just after the positive-gain ones.


Pitfall 2: Sorting the values instead of the indices

A tempting shortcut is to sort the gains directly:

# WRONG: loses the link to technique1[i] / technique2[i]
gains = sorted((technique1[i] - technique2[i] for i in range(n)), reverse=True)

After this you only have the difference, not the original pair, so you can no longer correctly decide which contribution to add. Worse, when you later need technique1[i] and technique2[i] separately for the optional phase, the mapping is gone.

Fix: Sort the indices by gain (as the solution does with sorted(range(n), ...)), or sort (gain, t1, t2) tuples together so each element keeps its full information.


Pitfall 3: Double-counting or off-by-one when handling the forced vs. optional split

A subtle bug is processing the same index in both loops, or slicing incorrectly:

for i in sorted_indices[:k]:      # forced
    ...
for i in sorted_indices[k - 1:]:  # WRONG: re-processes index at position k-1
    ...

This would apply two swaps to the boundary task, corrupting the total.

Fix: Use disjoint, contiguous slices — sorted_indices[:k] and sorted_indices[k:] — so every index is processed exactly once. Also validate the input so that 0 <= k <= n; an out-of-range k would silently misbehave with Python slicing (e.g., k > n would force fewer than expected without raising an error).

# Defensive guard
k = max(0, min(k, n))

This clamps k into a valid range and keeps the two-phase logic correct.

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:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).


Recommended Readings

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

Load More