2938. Separate Black and White Balls


Problem Description

In this problem, we're given n balls on a table, colored black or white, and represented by a binary string s of length n. The character 1 represents a black ball, while 0 represents a white ball. The objective is to group all the black balls to the right side of the string and all the white balls to the left side, using a series of adjacent swaps. Each step allows a swap of two neighboring balls. The final goal is to determine the minimum number of such swaps needed to achieve the segregation of the balls into their respective groups by color.

Intuition

The key insight behind the solution is to count the number of steps it takes to bring each black ball (represented by 1) to its final position on the right side. Due to the nature of the swaps, every time we move a black ball one position to the right, one swap is required. However, as we progress in grouping the black balls to the right, each successive black ball we encounter will have to be moved a few positions less than the previous one because of the black balls that we have already moved.

To calculate the number of moves efficiently, we simulate this process in reverse order by iterating from the right to the left. We use a variable cnt to keep track of the black balls that have been encountered, and we use ans to sum up the moves needed. With every black ball encountered, we increase cnt by one since one more black ball is now in its right-most position. We add n - i - cnt to ans each time we find a black ball because the ball needs to pass over n - i - cnt black balls that have already been placed to the right.

By moving from right to left, we can calculate the minimum steps with a single pass through the string, making the process efficient and avoiding the need for simulating actual swaps.

Learn more about Greedy and Two Pointers patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

In a binary min heap, the maximum element can be found in:

Solution Approach

The implementation of the solution starts by initializing two variables, cnt and ans, both set to 0. The variable cnt will keep track of the number of black balls (1s) we have encountered starting from the rightmost side, while ans will accumulate the total number of steps (swaps) required to move all black balls to the right.

We begin with a reverse iteration of the string, from the last character to the first (index n-1 to 0). We traverse in reverse because we're interested in calculating the number of steps a black ball needs to move from its current position to its final destination, which we can easily compute once we know how many black balls are already positioned to the right of it.

During the iteration, for each black ball we encounter, which is marked by a 1 in string s:

  • We increment cnt by 1, representing another black ball that is in its appropriate position on the right.
  • We calculate n - i - cnt, which gives us the number of swaps needed to move this particular black ball past the cnt black balls already in place. Note that i is the current index in the iteration. We do this because each black ball already placed means one less swap needed for the current one.
  • We add this calculated number of swaps to ans, which is our total.

The algorithm does not use auxiliary data structures, thereby maintaining a space complexity of O(1). The time complexity is O(n), as we only need to traverse the string once to count the necessary swaps.

Here's the reference approach, expressed as pseudocode to describe the process:

1function minimumSteps(s: string) -> integer:
2    n = length(s)
3    cnt = 0
4    ans = 0
5    for i from n-1 to 0:
6        if s[i] == '1':
7            cnt = cnt + 1
8            ans = ans + (n - i - cnt)
9    return ans

The beauty of this solution lies in its simplicity. By intelligently choosing to iterate in reverse and keep track of the number of encountered black balls, we can calculate the solution in a single pass without simulating the swaps, which could have been significantly costlier in terms of performance.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Example Walkthrough

Let's walk through an example to better understand the solution approach using the problem content above.

Imagine we have the binary string 10101, representing 5 balls where the 1s are black balls, and the 0s are white balls. Using the solution approach, we want to calculate the minimum number of swaps needed to group all black balls to the right.

Following the steps of the algorithm:

  1. We start with cnt and ans initialized to 0.
  2. The string 10101 has length n = 5.
  3. We iterate in reverse, starting from the last character (index 4) to the first (index 0).
    • i = 4, s[4] = 1 (black ball): We increment cnt to 1. ans becomes ans + (5 - 4 - 1) which is ans + 0 (no swap needed because it is already in the last position).
    • i = 3, s[3] = 0 (white ball): No action taken because we only count swaps for black balls. cnt and ans are unchanged.
    • i = 2, s[2] = 1 (black ball): We increment cnt to 2. ans becomes ans + (5 - 2 - 2) which is ans + 1 (one swap needed to move to the end).
    • i = 1, s[1] = 0 (white ball): Again, no action is needed for white balls. No changes to cnt or ans.
    • i = 0, s[0] = 1 (black ball): Increment cnt to 3. ans becomes ans + (5 - 0 - 3) which is ans + 2 (two swaps needed to move to the end).
  4. Adding these up, ans = 0 + 1 + 2 which equals 3.

Therefore, for the given string 10101, it will take a minimum of 3 swaps to move all the black balls to the right side of the string. This matches the logic of our solution approach. The black balls initially at indices 0 and 2 need to each move past two white balls to reach the far right, with the last black ball at index 4 already in the correct position.

Solution Implementation

1class Solution:
2    def minimumSteps(self, s: str) -> int:
3        # Length of the given string
4        length = len(s)
5
6        # Initialize 'answer' for holding the minimum steps and
7        # 'one_count' for keeping track of the number of '1's encountered
8        answer = 0
9        one_count = 0
10
11        # Traverse the string in reverse from last to first character
12        for i in range(length - 1, -1, -1):
13            # Check if the current character is '1'
14            if s[i] == '1':
15                # If it's '1', increment the 'one_count'
16                one_count += 1
17                # Update answer by how many steps needed to move this '1' to the end
18                # considering the number of ones already counted.
19                answer += (length - i - 1) - one_count + 1
20
21        # Return the total minimum steps calculated
22        return answer
23
1class Solution {
2    /**
3     * Calculates the minimum number of steps to move all '1's to the right end of the string
4     *
5     * @param s the input string representing a binary number
6     * @return the minimum number of steps required
7     */
8    public long minimumSteps(String s) {
9        // Initialize the answer to 0
10        long answer = 0;
11        // Counter for the number of '1's found
12        int oneCount = 0;
13        // Length of the string
14        int length = s.length();
15
16        // Iterate over the string from right to left
17        for (int i = length - 1; i >= 0; --i) {
18            // Check if the current character is '1'
19            if (s.charAt(i) == '1') {
20                // Increase the count of '1's
21                ++oneCount;
22                // Add the number of steps to move this '1' to the right end
23                answer += length - i - oneCount;
24            }
25        }
26        // Return the total number of steps calculated
27        return answer;
28    }
29}
30
1#include <string>
2
3class Solution {
4public:
5    // Function to calculate the minimum number of steps required.
6    long long minimumSteps(std::string s) {
7        long long steps = 0; // Variable to store the minimum steps.
8        int onesCount = 0; // Counter for the occurrences of '1'.
9        int length = s.size(); // Get the size of the string.
10
11        // Traverse the string from the end to the beginning.
12        for (int i = length - 1; i >= 0; --i) {
13            if (s[i] == '1') { // If the current character is '1'.
14                ++onesCount; // Increment the count of '1's.
15
16                // Accumulate the distance from right-most end minus
17                // the number of '1's encountered so far.
18                steps += length - i - onesCount;
19            }
20        }
21        // Return the computed minimum steps.
22        return steps;
23    }
24};
25
1function minimumSteps(s: string): number {
2    const stringLength = s.length;          // Get the length of the input string.
3    let steps = 0;                          // Initialize the steps counter to zero.
4    let countOfOnes = 0;                    // Initialize a counter to keep track of the number of '1's encountered.
5
6    // Iterate over the string in reverse order.
7    for (let i = stringLength - 1; i >= 0; --i) {
8        // The '~' before 'i' is a bitwise NOT operator, used here as a shorthand for (i !== -1).
9        if (s[i] === '1') {
10            // If the current character is '1', increment the count of '1's.
11            ++countOfOnes;
12            // Accumulate the necessary steps to bring the '1' to the end of the string, taking into account
13            // the count of '1's encountered so far, which do not need to be moved.
14            steps += stringLength - i - countOfOnes;
15        }
16    }
17
18    // Return the total number of steps required to move all '1's to the end of the string.
19    return steps;
20}
21
Not Sure What to Study? Take the 2-min Quiz:

How does merge sort divide the problem into subproblems?

Time and Space Complexity

Time Complexity

The given code involves a single for-loop that iterates over the string s in reverse order, starting from the last character to the first. Since each element of the string s is processed exactly once during this iteration, the time complexity of this operation is O(n), where n is the length of the string s. Inside the loop, only constant time operations are performed (i.e., basic arithmetic operations and conditional checks), which do not depend on the size of the input, therefore they don't affect the overall linear time complexity.

Space Complexity

The algorithm uses a fixed number of variables (n, ans, cnt, and i) that do not scale with the size of the input. This implies that the space complexity is constant. Therefore, the space complexity of the code is O(1), as the amount of memory used does not increase with the size of the input string s.

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫