title: All Divisions With the Highest Score of a Binary Array
slug: all-divisions-with-highest-score-of-binary-array
difficulty: medium
leetcode_id: 2155
leetcode_url: https://leetcode.com/problems/all-divisions-with-the-highest-score-of-a-binary-array/
categories:
  - arrays
patterns:
  - prefix-sum

function_signature: "def max_score_indices(nums: list[int]) -> list[int]:"

test_cases:
  visible:
    - input: { nums: [0, 0, 1, 0] }
      expected: [2, 4]
    - input: { nums: [0, 0, 0] }
      expected: [3]
    - input: { nums: [1, 1] }
      expected: [0]
  hidden:
    - input: { nums: [0] }
      expected: [1]
    - input: { nums: [1] }
      expected: [0]
    - input: { nums: [0, 1, 0, 1] }
      expected: [2]
    - input: { nums: [1, 0, 1, 0, 1] }
      expected: [0, 2, 4]
    - input: { nums: [0, 0, 0, 0] }
      expected: [4]

description: |
  You are given a **0-indexed** binary array `nums` of length `n`. `nums` can be divided at index `i` (where `0 <= i <= n`) into two arrays (possibly empty) `nums_left` and `nums_right`:

  - `nums_left` has all the elements of `nums` between index `0` and `i - 1` **(inclusive)**, while `nums_right` has all the elements of `nums` between index `i` and `n - 1` **(inclusive)**.
  - If `i == 0`, `nums_left` is **empty**, while `nums_right` has all the elements of `nums`.
  - If `i == n`, `nums_left` has all the elements of `nums`, while `nums_right` is **empty**.

  The **division score** of an index `i` is the **sum** of the number of `0`'s in `nums_left` and the number of `1`'s in `nums_right`.

  Return *all distinct indices that have the **highest** possible **division score***. You may return the answer in **any order**.

constraints: |
  - `n == nums.length`
  - `1 <= n <= 10^5`
  - `nums[i]` is either `0` or `1`

examples:
  - input: "nums = [0,0,1,0]"
    output: "[2,4]"
    explanation: "Division at index 2: nums_left is [0,0], nums_right is [1,0]. Score is 2 + 1 = 3. Division at index 4: nums_left is [0,0,1,0], nums_right is []. Score is 3 + 0 = 3. Both indices achieve the highest score of 3."
  - input: "nums = [0,0,0]"
    output: "[3]"
    explanation: "Division at index 3: nums_left is [0,0,0], nums_right is []. Score is 3 + 0 = 3. Only index 3 achieves the highest score."
  - input: "nums = [1,1]"
    output: "[0]"
    explanation: "Division at index 0: nums_left is [], nums_right is [1,1]. Score is 0 + 2 = 2. Only index 0 achieves the highest score."

explanation:
  intuition: |
    Imagine you're drawing a vertical line through an array, dividing it into left and right portions. You want to maximise a score where you **count zeros on the left** and **ones on the right**.

    The key insight is that as you move the division point from left to right:
    - When you encounter a `0`, moving it from right to left **increases** your score (one more zero on the left)
    - When you encounter a `1`, moving it from right to left **decreases** your score (one fewer one on the right)

    Think of it like this: start with the division at index `0` (everything on the right). Your initial score is the total count of `1`s in the array. As you slide the division point rightward, each `0` you pass adds `+1` to your score, and each `1` you pass subtracts `-1` from your score.

    This means you don't need to recalculate counts from scratch at each position — you can compute the score incrementally in a single pass using prefix sum logic.

  approach: |
    We solve this using a **Single Pass with Running Score**:

    **Step 1: Calculate the initial score**

    - Count all `1`s in the array — this is the score when the division is at index `0` (empty left, full right)
    - `ones_right`: Total count of `1`s in the array
    - `zeros_left`: Initially `0` since left portion is empty

    &nbsp;

    **Step 2: Track the maximum and collect indices**

    - Initialise `max_score` to the initial score (at index `0`)
    - Initialise `result` list with `[0]` since index `0` starts with the max score

    &nbsp;

    **Step 3: Iterate through possible division points**

    - For each index `i` from `1` to `n`:
      - Look at the element that just moved from right to left: `nums[i-1]`
      - If `nums[i-1] == 0`: increment `zeros_left` (score increases by 1)
      - If `nums[i-1] == 1`: decrement `ones_right` (score decreases by 1)
      - Calculate `current_score = zeros_left + ones_right`
      - If `current_score > max_score`: update `max_score` and reset `result` to `[i]`
      - If `current_score == max_score`: append `i` to `result`

    &nbsp;

    **Step 4: Return the result**

    - Return the list of all indices that achieved `max_score`

  common_pitfalls:
    - title: Recalculating Counts at Each Position
      description: |
        A naive approach recounts zeros and ones for each division point:
        - For each `i`, count zeros in `nums[0:i]` and ones in `nums[i:n]`

        This results in **O(n^2) time complexity**. With `n <= 10^5`, this means up to 10 billion operations — too slow.

        Instead, recognise that moving the division by one position only changes the count by one element. Use running counters that update in O(1) time.
      wrong_approach: "Nested loops counting zeros and ones for each division"
      correct_approach: "Single pass updating running counters incrementally"

    - title: Off-by-One Errors with Division Points
      description: |
        There are `n + 1` valid division points (indices `0` through `n`), not `n`.

        - Index `0`: empty left, full right
        - Index `n`: full left, empty right

        When iterating, make sure to include the final division point at index `n`. A common mistake is iterating only to `n - 1`.
      wrong_approach: "Iterating from 0 to n-1"
      correct_approach: "Iterating from 0 to n (inclusive)"

    - title: Forgetting to Handle Ties
      description: |
        The problem asks for **all** indices with the highest score, not just one.

        When you find a score equal to the current maximum, you must append the index to your result list rather than replace it. Use separate logic for "new maximum found" versus "tied with maximum".

  key_takeaways:
    - "**Prefix sum pattern**: When a score depends on counts of elements on either side of a moving boundary, track running totals instead of recounting"
    - "**Incremental updates**: Moving a boundary by one position changes the score by exactly one element — exploit this for O(1) updates"
    - "**Tracking multiple maxima**: When collecting all indices that achieve a maximum, distinguish between finding a new max (reset list) and tying (append to list)"
    - "**Division point indexing**: Remember that dividing an array of length `n` creates `n + 1` possible division points"

  time_complexity: "O(n). We make one pass to count ones, then one pass to compute scores at each division point."
  space_complexity: "O(1) auxiliary space (excluding the output list). We only use a few integer variables for tracking counts and scores."

solutions:
  - approach_name: Single Pass with Running Score
    is_optimal: true
    code: |
      def max_score_indices(nums: list[int]) -> list[int]:
          n = len(nums)

          # Initial score: division at index 0 (empty left, full right)
          # Score = zeros on left (0) + ones on right (total ones)
          ones_right = sum(nums)  # Count all 1s
          zeros_left = 0

          max_score = ones_right  # Score at division index 0
          result = [0]  # Index 0 starts with the max

          # Try each division point from 1 to n
          for i in range(1, n + 1):
              # Element nums[i-1] moves from right to left
              if nums[i - 1] == 0:
                  zeros_left += 1  # One more zero on the left
              else:
                  ones_right -= 1  # One fewer one on the right

              current_score = zeros_left + ones_right

              if current_score > max_score:
                  # Found a new maximum - reset the result list
                  max_score = current_score
                  result = [i]
              elif current_score == max_score:
                  # Tied with maximum - add to result list
                  result.append(i)

          return result
    explanation: |
      **Time Complexity:** O(n) — One pass to count ones, one pass to compute scores.

      **Space Complexity:** O(1) auxiliary — Only integer variables for counters (output list not counted).

      We start with the division at index 0 and incrementally update our counters as we move the boundary rightward. Each element that crosses from right to left either adds 1 (if it's a zero) or subtracts 1 (if it's a one) from the score.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def max_score_indices(nums: list[int]) -> list[int]:
          n = len(nums)
          scores = []

          # Calculate score for each division point
          for i in range(n + 1):
              # Count zeros in left portion [0, i)
              zeros_left = nums[:i].count(0)
              # Count ones in right portion [i, n)
              ones_right = nums[i:].count(1)
              scores.append(zeros_left + ones_right)

          # Find maximum score and all indices that achieve it
          max_score = max(scores)
          return [i for i, score in enumerate(scores) if score == max_score]
    explanation: |
      **Time Complexity:** O(n^2) — For each of n+1 division points, we count elements in O(n) time.

      **Space Complexity:** O(n) — We store all n+1 scores.

      This approach directly implements the problem definition but is too slow for large inputs. Each `count()` call scans a portion of the array, leading to quadratic time. Included to illustrate why incremental counting is necessary.
