Facebook Pixel

3781. Maximum Score After Binary Swaps

MediumGreedyArrayStringHeap (Priority Queue)
LeetCode ↗

Problem Description

You are given an integer array nums of length n and a binary string s of the same length.

Initially, your score is 0. Each index i where s[i] = '1' contributes nums[i] to the score.

You may perform any number of operations (including zero). In one operation, you may choose an index i such that 0 <= i < n - 1, where s[i] = '0' and s[i + 1] = '1', and swap these two characters.

Return an integer denoting the maximum possible score you can achieve.

In other words, each '1' in the string can be moved to the left through any number of swaps (each swap moves a '1' one position left, past a '0'). After rearranging the '1's by moving them leftward, the score is the sum of nums[i] over all positions i where s[i] = '1'. The goal is to choose how to perform the swaps so that this total sum is as large as possible.

The key observation is that because a '1' can only move left and cannot move right, each '1' ends up occupying some position at or before its original location. Since '1's can be relocated to any earlier available spots, each '1' effectively gets to select one of the numbers at indices to its left (including its own index), without two '1's sharing the same index. To maximize the score, every '1' should claim the largest still-unclaimed value among the indices available to it.

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

Using a max-heap to greedily assign each '1' the largest available value it can reach by moving left maximizes the total score.

Open in Flowchart

Intuition

Let's think about what the swap operation really lets us do. The only allowed swap moves a '1' one step to the left (past a '0'). A '1' can never move to the right. This means every '1' can end up at its original position or at any position before it, as long as no two '1's land on the same spot.

So the real question becomes: for each '1' in the string, which index's value should it collect? Since a '1' at position i can slide left to any earlier empty slot, it is free to "pick up" the value nums[j] for any index j <= i that isn't already taken by another '1'.

If we process the array from left to right, then by the time we reach the k-th '1', all indices up to its current position have already been seen. Together with the values from earlier '1's positions, this forms a pool of candidate numbers. Each '1' wants the biggest value it can grab, and earlier choices should never block later ones from doing better.

This naturally leads to a greedy idea: keep a running collection of all values seen so far, and whenever we hit a '1', take the largest value currently available. Because every value added stays a valid candidate for all '1's that come afterward, always grabbing the current maximum for each '1' guarantees the overall sum is maximized.

To efficiently fetch and remove the largest available value at each '1', a max-heap is the perfect tool. We push each nums[i] into the heap as we scan, and for every '1' we pop the heap's maximum and add it to the answer. Since Python's heapq is a min-heap, we push negated values -x so that popping gives us the largest original number.

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

Solution Approach

Solution 1: Max-Heap

According to the problem statement, each '1' can be swapped left any number of times, so each '1' can choose the largest unpicked number among the indices to its left (including its own). We maintain these candidate numbers with a max-heap.

We traverse the string s together with the array nums from left to right. For each position i:

  1. Push the current value nums[i] into the max-heap. This makes it a candidate that can be picked by the current '1' or any later '1'.
  2. If s[i] = '1', pop the maximum value from the heap and add it to the answer. This represents the current '1' grabbing the best still-available value.

After the traversal finishes, the accumulated sum is the maximum score.

Since Python's heapq only provides a min-heap, we store the negated values -x. Pushing -x and popping the smallest element gives us the largest original number, so we subtract the popped value (ans -= heappop(pq)) to add its true positive amount.

Walking through the code:

  • pq = [] initializes an empty heap, and ans = 0 initializes the score.
  • heappush(pq, -x) adds every number as a candidate as we scan.
  • When c == "1", ans -= heappop(pq) removes the current maximum and adds it to the answer.
  • Finally, return ans gives the maximum achievable score.

The time complexity is O(n × log n), where n is the length of nums, because each element is pushed and possibly popped from the heap once, and each heap operation costs O(log n). The space complexity is O(n) for the heap.

Example Walkthrough

Let's trace through a small example to see the max-heap approach in action.

Input:

  • nums = [3, 1, 5, 2]
  • s = "0110"

Our goal is to move each '1' leftward (or leave it in place) so that the sum of values at the '1' positions is maximized. There are two '1's (at indices 1 and 2), so we will end up picking two values.

We scan from left to right, maintaining a max-heap (pq) of candidate values seen so far and a running ans = 0.

inums[i]s[i]ActionHeap after action (as values)ans
03'0'Push 3[3]0
11'1'Push 1 → heap [3, 1]; pop max (3), add to ans[1]3
25'1'Push 5 → heap [5, 1]; pop max (5), add to ans[1]8
32'0'Push 2[2, 1]8

Step-by-step reasoning:

  1. i = 0s[0] = '0', so no '1' to satisfy yet. We still push nums[0] = 3 into the heap because it remains a valid candidate for any future '1' (a later '1' could slide left onto index 0).
  2. i = 1 — first push nums[1] = 1. The heap now holds {3, 1}. Since s[1] = '1', this '1' grabs the largest available value. It picks 3 (which lives at index 0, reachable by sliding left). ans = 3. The value 1 stays in the heap.
  3. i = 2 — push nums[2] = 5. The heap now holds {5, 1}. Since s[2] = '1', this '1' grabs the maximum, 5 (its own position). ans = 8. The value 1 remains unclaimed.
  4. i = 3s[3] = '0', so we only push nums[3] = 2. No '1' to satisfy.

Result: ans = 8.

Verification against the problem semantics: The first '1' (index 1) slides left into index 0 to collect nums[0] = 3. The second '1' (index 2) stays put to collect nums[2] = 5. Final placement corresponds to '1's at indices 0 and 2, giving 3 + 5 = 8 — the maximum possible. Any other assignment (e.g., keeping the first '1' at index 1 for value 1) would yield a smaller sum, confirming the greedy "always take the current max" strategy is optimal.

Note how the implementation uses negated values internally: pushing -3, -1, -5 into Python's min-heap and popping the smallest (-3, then -5) recovers the largest originals via ans -= heappop(pq).

Solution Implementation

1from heapq import heappush, heappop
2from typing import List
3
4
5class Solution:
6    def maximumScore(self, nums: List[int], s: str) -> int:
7        # Running total of selected values
8        total_score = 0
9
10        # Max-heap simulated with a min-heap by storing negated values
11        max_heap = []
12
13        # Traverse each number together with its corresponding flag character
14        for num, flag in zip(nums, s):
15            # Push the negated number so the smallest negative (largest original) is on top
16            heappush(max_heap, -num)
17
18            # When the flag is "1", we are allowed to take one number:
19            # greedily pick the largest seen so far
20            if flag == "1":
21                total_score -= heappop(max_heap)  # subtract negative => add original
22
23        return total_score
24
1class Solution {
2    public long maximumScore(int[] nums, String s) {
3        // Accumulator for the final maximum score
4        long totalScore = 0;
5
6        // Max-heap to keep the largest available numbers at the top.
7        // Collections.reverseOrder() turns the default min-heap into a max-heap.
8        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
9
10        // Iterate through every number alongside its corresponding flag in the string.
11        for (int i = 0; i < nums.length; i++) {
12            int currentNum = nums[i];
13            char flag = s.charAt(i);
14
15            // Add the current number into the pool of candidates.
16            maxHeap.offer(currentNum);
17
18            // When the flag is '1', we are allowed to "collect" one number.
19            // Greedily take the largest available number to maximize the score.
20            if (flag == '1') {
21                totalScore += maxHeap.poll();
22            }
23        }
24
25        return totalScore;
26    }
27}
28
1class Solution {
2public:
3    long long maximumScore(vector<int>& nums, string s) {
4        // Accumulated maximum score
5        long long totalScore = 0;
6
7        // Max-heap to keep track of the largest available numbers seen so far
8        priority_queue<int> maxHeap;
9
10        // Iterate through each number paired with its corresponding character in s
11        for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
12            int currentValue = nums[i];
13            char currentFlag = s[i];
14
15            // Add the current number into the pool of candidates
16            maxHeap.push(currentValue);
17
18            // When the flag is '1', we are allowed to "collect" one number.
19            // Greedily pick the largest available value to maximize the score.
20            if (currentFlag == '1') {
21                totalScore += maxHeap.top();
22                maxHeap.pop();
23            }
24        }
25
26        return totalScore;
27    }
28};
29
1/**
2 * Computes the maximum score by greedily selecting the largest available numbers.
3 * At each position where the character in `s` is '1', the current maximum value
4 * from all numbers seen so far (that haven't been used) is added to the answer.
5 *
6 * @param nums - The array of numbers to choose from.
7 * @param s - A binary string where '1' indicates a position to take the max value.
8 * @returns The accumulated maximum score.
9 */
10function maximumScore(nums: number[], s: string): number {
11    // Accumulated result score.
12    let answer = 0;
13
14    // Max-heap holding all numbers encountered so far that are still available.
15    const maxHeap = new MaxPriorityQueue<number>();
16
17    for (let i = 0; i < nums.length; i++) {
18        const value = nums[i];
19        const flag = s[i];
20
21        // Make the current number available for selection.
22        maxHeap.enqueue(value);
23
24        // When the flag is '1', take the largest available number.
25        if (flag === '1') {
26            answer += maxHeap.dequeue()!;
27        }
28    }
29
30    return answer;
31}
32

Time and Space Complexity

  • Time Complexity: O(n log n), where n is the length of the array nums. The code iterates through nums and s together with a single loop running n times. In each iteration, a heappush operation is performed, costing O(log n). Additionally, when the character c equals "1", a heappop operation is also performed, also costing O(log n). Therefore, the overall time complexity is O(n log n).

  • Space Complexity: O(n), where n is the length of the array nums. The priority queue pq can hold up to n elements in the worst case, so the additional space required is O(n).

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

Common Pitfalls

A frequent mistake is forgetting that Python's heapq is a min-heap and mishandling the sign conversion, which breaks the greedy "pick the largest available value" logic.

Pitfall 1: Treating the heap as a max-heap directly

Python only provides a min-heap. If you push the raw values and pop, you will always remove the smallest candidate instead of the largest:

# WRONG: this pops the smallest value, minimizing the score
heappush(max_heap, num)
if flag == "1":
    total_score += heappop(max_heap)   # picks the worst candidate

This produces the minimum score, not the maximum. The fix is to negate values on the way in and on the way out:

# CORRECT: store negated values to simulate a max-heap
heappush(max_heap, -num)
if flag == "1":
    total_score -= heappop(max_heap)   # subtract negative => add original positive

Pitfall 2: Inconsistent sign handling

Another variant of the same bug is negating only on push but forgetting to negate on pop (or vice versa):

# WRONG: pushes -num but adds the negative value directly
heappush(max_heap, -num)
if flag == "1":
    total_score += heappop(max_heap)   # adds a negative number, shrinking the score

Here the largest original value is correctly retrieved (the most negative entry pops first), but it is added back as a negative number. Always keep the negation symmetric: negate when pushing and negate again when popping.

# CORRECT
heappush(max_heap, -num)
total_score -= heappop(max_heap)       # the two negations cancel out

Pitfall 3: Pushing only when the character is '1'

It is tempting to push nums[i] into the heap only at positions where s[i] == '1', but every index to the left of a '1' is a valid candidate—including positions holding '0'. A '1' can be swapped leftward into a '0' slot, so those values must be available too:

# WRONG: skips '0' positions, losing valid candidates
for num, flag in zip(nums, s):
    if flag == "1":
        heappush(max_heap, -num)
        total_score -= heappop(max_heap)

The correct approach pushes every value as you scan, and only pops when you encounter a '1':

# CORRECT
for num, flag in zip(nums, s):
    heappush(max_heap, -num)           # every index is a candidate
    if flag == "1":
        total_score -= heappop(max_heap)

Key takeaway

The greedy correctness relies on two invariants: (1) the heap must always contain all values seen so far (not just '1' positions), and (2) each '1' must extract the current maximum. Both the sign-handling and the unconditional-push details are essential to preserve these invariants.

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:

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

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

Load More