Taro Logo

Maximum Number of Operations to Move Ones to the End

Medium
Asked by:
Profile picture
21 views
Topics:
StringsGreedy Algorithms

You are given a binary string s.

You can perform the following operation on the string any number of times:

  • Choose any index i from the string where i + 1 < s.length such that s[i] == '1' and s[i + 1] == '0'.
  • Move the character s[i] to the right until it reaches the end of the string or another '1'. For example, for s = "010010", if we choose i = 1, the resulting string will be s = "000110".

Return the maximum number of operations that you can perform.

Example 1:

Input: s = "1001101"

Output: 4

Explanation:

We can perform the following operations:

  • Choose index i = 0. The resulting string is s = "0011101".
  • Choose index i = 4. The resulting string is s = "0011011".
  • Choose index i = 3. The resulting string is s = "0010111".
  • Choose index i = 2. The resulting string is s = "0001111".

Example 2:

Input: s = "00111"

Output: 0

Constraints:

  • 1 <= s.length <= 105
  • s[i] is either '0' or '1'.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What are the possible values in the input array? Can it contain values other than 0 and 1?
  2. If the input array is empty or contains only ones or only zeros, what should the function return?
  3. Is the order of the zeros before the ones in the original array important to preserve after the operations?
  4. Are there any constraints on the number of operations that can be performed?
  5. Could you provide a specific example of the input and expected output to confirm my understanding?

Brute Force Solution

Approach

The brute force method for moving all ones to the end involves systematically checking every possible series of swaps. We're trying every arrangement to see which one requires the fewest moves. It’s like shuffling a deck of cards until you find the order you want.

Here's how the algorithm would work step-by-step:

  1. Imagine we have a line of objects that are either zero or one.
  2. We can try swapping the first object with every other object in the line.
  3. Then, we try swapping the second object with every object after it.
  4. We keep doing this for every object in the line, trying out every possible swap.
  5. Each time we do a series of swaps, we count how many swaps it took to move all the ones to the end of the line.
  6. We then compare the number of swaps from each series of swaps we tried.
  7. Finally, we pick the series of swaps that used the fewest number of swaps to get all the ones to the end. That's the best answer.

Code Implementation

def maximum_operations_to_move_ones_to_end_brute_force(binary_array):
    array_length = len(binary_array)
    minimum_swaps = float('inf')

    # Iterate through all possible swap combinations
    for i in range(1 << (array_length * (array_length - 1) // 2)):
        temp_array = binary_array[:]
        swap_count = 0
        swap_index = 0

        # Generate the swap sequence based on the current iteration
        for first_index in range(array_length):
            for second_index in range(first_index + 1, array_length):
                # Determine if a swap should occur in the current iteration
                if (i >> swap_index) & 1:
                    # Perform the swap
                    temp_array[first_index], temp_array[second_index] = temp_array[second_index], temp_array[first_index]
                    swap_count += 1

                swap_index += 1

        # Check if all ones are at the end of the array.
        is_sorted = True
        first_one_index = -1

        for index in range(array_length):
            if temp_array[index] == 1:
                first_one_index = index
                break

        if first_one_index != -1:
            for index in range(first_one_index):
                if temp_array[index] == 1:
                    is_sorted = False
                    break

        if is_sorted:
            # We found a valid sequence
            minimum_swaps = min(minimum_swaps, swap_count)

    # If no valid sequence was found, return 0
    if minimum_swaps == float('inf'):
        return 0
    else:
        return minimum_swaps

Big(O) Analysis

Time Complexity
O(n³) – The brute force approach, as described, iterates through each element in the array of size n. For each element, it attempts a swap with every subsequent element (n-i elements in the i-th iteration). After each series of swaps, the solution checks if all ones are at the end, which requires scanning the entire array of size n. Thus, the number of swaps is on the order of n*(n-1)/2 and the verification is done at each step for a cost of n. Therefore, the total approximate operations is n * (n * (n-1)/2) and simplifies to O(n³).
Space Complexity
O(N) – The brute force method, as described, explores every possible series of swaps by repeatedly creating copies of the input array to perform different swap sequences. For each series of swaps, the algorithm needs to store a modified version of the input array. Since we may need to store a modified array with N elements for each series of swaps, the auxiliary space required grows linearly with the input size N, where N is the number of elements in the input array of zeros and ones. Therefore, the space complexity is O(N).

Optimal Solution

Approach

The goal is to shift all the ones to the end of a series with the fewest possible moves. We can achieve this by focusing on pairing up the unavoidable moves and cancelling them out whenever possible. This eliminates the need to perform unnecessary individual move operations.

Here's how the algorithm would work step-by-step:

  1. Count the total number of ones in the series.
  2. Imagine all the ones should ideally sit at the very end of the series.
  3. Start looking at the beginning of the series. Notice any zeros that are currently located where a one should eventually be.
  4. Simultaneously, start looking at the end of the series going backward. Notice any ones that are located where a zero should eventually be.
  5. For each zero found at the front, pair it up with a one found at the back. This represents a single move that needs to occur.
  6. Keep pairing zeros at the front with ones at the back. Each pair represents a single operation needed.
  7. The total number of pairs formed represents the minimum number of operations to move all ones to the end.

Code Implementation

def min_operations_to_move_ones_to_end(series):
    number_of_ones = series.count(1)
    left_index = 0
    right_index = len(series) - 1
    operations_count = 0

    while left_index < len(series) - number_of_ones and right_index >= 0:
        # Find a zero from the left that needs to be moved.
        while left_index < len(series) - number_of_ones and series[left_index] != 0:
            left_index += 1

        # Find a one from the right that needs to be moved.
        while right_index >= len(series) - number_of_ones and series[right_index] != 1:
            right_index -= 1

        # Increment count only if both indices are still valid.
        if left_index < len(series) - number_of_ones and right_index >= len(series) - number_of_ones:
            operations_count += 1
            left_index += 1
            right_index -= 1

    return operations_count

Big(O) Analysis

Time Complexity
O(n) – The algorithm iterates through the series once to count the total number of ones. Then, it effectively uses two pointers, one starting from the beginning and the other from the end, to find zeros at the front and ones at the back for pairing. In the worst-case scenario, each pointer traverses the entire series once. Therefore, the time complexity is determined by these linear traversals of the input series, which is O(n).
Space Complexity
O(1) – The algorithm's space complexity is determined by the variables used to store the count of ones, and potentially pointers or indices used to traverse the series. The provided plain English explanation does not use any auxiliary data structures like lists, arrays, or hash maps. Therefore, the space required remains constant irrespective of the size (N) of the series. The extra space used is independent of the input size, resulting in O(1) space complexity.

Edge Cases

Empty input array
How to Handle:
Return 0 immediately since there are no elements to move.
Array with only one element
How to Handle:
Return 0 since there's nothing to move.
Array contains only ones
How to Handle:
Return 0 as no swaps are needed.
Array contains no ones
How to Handle:
Return 0 as no ones can be moved.
Array with all zeros followed by all ones
How to Handle:
Return 0 as the array is already optimally arranged.
Array with all ones followed by all zeros
How to Handle:
Return the number of zeros in the array, which are the necessary swaps.
Large array with interleaved zeros and ones
How to Handle:
The algorithm should iterate through the array efficiently (O(n)) to count zeros before the last one.
Integer overflow if array size is extremely large, resulting in inaccurate count.
How to Handle:
Use a data type that can accommodate large array sizes, like long in Java, to store the count.