209 lines
9.6 KiB
YAML
209 lines
9.6 KiB
YAML
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
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**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.
|