Q4 - Battery Pack Balance Point

Scenario: A Tesla battery pack is a series string of cells, each holding some amount of charge (given in pack order). For diagnostics you want to find a balance point: a single cell, the fulcrum, such that the total charge in all the cells to its left equals the total in all the cells to its right. The fulcrum cell is the pivot and counts for neither side.

Part 1: Given charges (the per-cell charge in order), return the index of the leftmost balance point, or -1 if no cell balances the pack.

Example 1 (Part 1: an exact balance point)

Input:  charges = [1, 7, 3, 6, 5, 6]
Output: 3
Why: at index 3 the cells to the left (1 + 7 + 3 = 11) match the cells to the
        right (5 + 6 = 11). The fulcrum cell (the 6 at index 3) is not counted.

Example 2 (Part 1: no balance point)

Input:  charges = [1, 2, 3]
Output: -1
Why: no single cell splits the rest into two equal-charge sides.

Part 1: Sweep the Fulcrum Once

The naive move is, for each candidate cell, to sum everything on its left and everything on its right and compare. That is a full pass per cell, so O(n^2). The insight is that as the fulcrum slides right, the left sum only ever grows by the cell it passes, and the right sum is whatever charge is left over.

Step 1: The right side is free once you know the left

Compute the pack's total once. For a fulcrum at index i, the right side is everything except the left part and the fulcrum itself: right = total - left - charges[i]. So a single running left gives you both sides at every position.

charges = [1, 7, 3, 6, 5, 6]   total = 28

at index i:   left = sum of charges[0 .. i-1]
              right = total - left - charges[i]

Step 2: Slide left to right, growing the running total

Start with left = 0. At each index, check whether left == right; if so, that is the balance point. Otherwise add the current charge to left and move on.

i=0 (1):  left 0,  right 28-0-1  = 27   ->  0  != 27
i=1 (7):  left 1,  right 28-1-7  = 20   ->  1  != 20
i=2 (3):  left 8,  right 28-8-3  = 17   ->  8  != 17
i=3 (6):  left 11, right 28-11-6 = 11   ->  11 == 11   balance point!
left = 0
for i, charge in enumerate(charges):
    right = total - left - charge      # everything except the left part and the fulcrum
    if left == right:
        return i
    left += charge

Here is the sweep on Example 1. The blue cells are the left side, the green cells are the right side, and the gold fulcrum slides across until the two sides match:

Fulcrum at index 0: no cells to the left (left 0), the rest sum to 27, imbalance 27
1 / 7
# O(n) time | O(1) space
def balance_point(charges):
    total = sum(charges)
    left = 0
    for i, charge in enumerate(charges):
        right = total - left - charge      # everything except the left part and the fulcrum
        if left == right:
            return i
        left += charge
    return -1

Part 2: The Closest Balance

Part 2: Real packs rarely split perfectly, so an exact balance point often does not exist. Extend the function to always return an answer: the index that comes closest to balanced (the smallest |left - right|), together with that minimum imbalance, as [index, imbalance].

Example 3 (Part 2)

Input:  charges = [3, 1, 4, 1, 5]
Output: [2, 2]
Why: no cell balances exactly. Index 2 comes closest: left = 3 + 1 = 4,
        right = 1 + 5 = 6, an imbalance of 2, smaller than at any other cell.

It is the same single sweep, with one change: instead of stopping at the first left == right, compute the imbalance |left - right| at every cell and keep the smallest one seen, along with its index. An exact balance point simply shows up as an imbalance of 0, so Part 2 fully contains Part 1.

charges = [3, 1, 4, 1, 5]   total = 14

i=0 (3):  left 0,  right 11   imbalance 11
i=1 (1):  left 3,  right 10   imbalance 7
i=2 (4):  left 4,  right 6    imbalance 2   <- smallest
i=3 (1):  left 8,  right 5    imbalance 3
i=4 (5):  left 9,  right 0    imbalance 9
best = [2, 2]
if gap < best_gap:                     # keep the smallest imbalance and where it happened
    best_gap, best_index = gap, i
# O(n) time | O(1) space
def best_balance(charges):
    total = sum(charges)
    left = 0
    best_index, best_gap = -1, float('inf')
    for i, charge in enumerate(charges):
        right = total - left - charge
        gap = abs(left - right)
        if gap < best_gap:             # keep the smallest imbalance and where it happened
            best_gap, best_index = gap, i
        left += charge
    return [best_index, best_gap]

Common Pitfalls

  • Recomputing both sides at every cell. Summing the left and right from scratch each time is the O(n^2) trap. One running left plus the total gives both sides in O(1) per cell.
  • Counting the fulcrum on a side. The balance-point cell belongs to neither side, so the right sum must subtract it: total - left - charges[i], not total - left.
  • Returning after adding the current charge. Check left == right before folding charges[i] into left; otherwise left already includes the fulcrum and the test is wrong by one cell.
  • Assuming an exact split exists. Part 1 can legitimately return -1. Part 2 sidesteps that by always reporting the closest cell, which is often what the real diagnostic wants anyway.

Complexity Analysis

Both parts: O(n) time, where n is the number of cells: one pass to sum the pack and one pass to sweep the fulcrum (or a single pass if you fold the total in first). Space is O(1), just the running left, the total, and the best seen so far. This is the whole reason the running-total trick matters: it takes the obvious quadratic idea down to linear time and constant space.

Final Notes

  1. The right side is the leftover. right = total - left - fulcrum is the whole trick. Saying that identity out loud, before writing any loop, is what shows you have seen past the naive double sum.
  2. One running total, one pass. Grow left as the fulcrum moves and you never look backward. That is what makes it O(n) time and O(1) space.
  3. Mind the fulcrum and the order of operations. The pivot cell counts for neither side, and you must test the balance before adding the current charge to left. Those two details are the classic off-by-one traps in this sweep.
  4. Part 2 is Part 1 with a running minimum. Track the smallest |left - right| instead of stopping at zero, and the exact case falls out as imbalance 0. Generalizing cleanly like this is a strong signal.
  5. Name the family. This is the prefix-sum / running-total pattern, the same idea behind pivot indexes, equilibrium points, and split-the-array problems. Recognizing it means the next variant is easy. 🚀

Was this page helpful?