Product except self: combine independent summariesLESSON 2.08 · 8 OF 43 IN CHAPTER
PART A / Coding problems and trade-offs
Step 34 of 252
LESSON 2.08 · 8 OF 43 IN CHAPTERTry it, then open the solution

Product except self: combine independent summaries

Constructed practice problem; no company attribution. Prerequisites: prefix summaries.

Candidate brief

THE PROBLEM

An analysis routine needs, at every position, the product of all other entries. Division is forbidden because zero is valid. Return a fresh list while using constant extra working storage beyond that output. What should one empty side contribute?

Write this:

def product_except_self(nums):
    ...
Contract Decision
Input Integer list/tuple; booleans excluded; negatives and zeros allowed
Output New list where output i is the product over every position except i
Boundaries Empty gives []; singleton gives [1]; input remains unchanged
Invalid input Invalid container or element raises ValueError
Excluded Division, floating-point stability, fixed-width integer arithmetic
Optional refresher · the underlying tool

The answer at position i is everything before it multiplied by everything after it. Two independent passes avoid division, which breaks on zeros:

nums = [2, 3, 4]
left_product = [1, 2, 6]  # product strictly before each position
right_product = [12, 4, 1]  # product strictly after each position
print([a*b for a,b in zip(left_product,right_product)])  # [12, 8, 6]

At index 1, neither side includes its own 3. Predict the result with one zero and with two zeros before reading the solution.

A design choice worth saying aloud

Choose whether the output array may temporarily hold left products; reusing it cuts extra storage without changing the contract. The running prefix_product and suffix_product stay strictly before and after the current index: neither may multiply nums[i] before writing the result at i. Zeros then work without a special division branch.

What the interviewer expects

The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.

Done means: New list where output i is the product over every position except i.

Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.

Test-case scenarios to settle before coding

01 · Representative

Input / starting state
[2,3,4]
Expected result
[12,8,6]

What it is testing: Each answer combines strict prefix and suffix.

02 · Singleton

Input / starting state
[7]
Expected result
[1]

What it is testing: The product of no other values is the multiplicative identity.

03 · One zero

Input / starting state
[0,3,4]
Expected result
[12,0,0]

What it is testing: Only the zero position sees the nonzero product.

04 · Two zeros

Input / starting state
[0,0,4]
Expected result
[0,0,0]

What it is testing: Every exclusion still contains a zero.

05 · Negative values

Input / starting state
[-1,2,-3]
Expected result
[-6,3,-2]

What it is testing: Signs follow ordinary integer multiplication.

06 · Invalid/atomic

Input / starting state
[1,False]
Expected result
ValueError; input unchanged

What it is testing: No division or silent boolean coercion.

For each case, show which branch or state change produces that result.

product_except_self([2, 3, 4]) == [12, 8, 6]. product_except_self([0, 3, 4]) == [12, 0, 0]; [0, 0, 4] gives [0, 0, 0]. product_except_self([7]) == [1]; product_except_self([False]) raises ValueError.

Before opening the explanation, restate the contract, trace the smallest useful example, implement a baseline, and identify the repeated work. Then implement your improvement independently and derive tests from the contract. Say what your state means before saying which data structure stores it.

Worked lesson, changed requirements, and reference

Split the excluded position out of the computation

The baseline multiplies every other element separately for each index: O(n²) multiplications and O(1) working space beyond output. Computing one total and dividing by the current element appears cheaper but violates the requirement and fails at zero. Ask instead which two disjoint pieces remain when position i is removed: everything strictly before it and everything strictly after it. Their products can be computed independently and combined.

i in [2, 3, 4] Product strictly left Product strictly right Output
0 1 12 12
1 2 4 8
2 6 1 6

The empty product is one because multiplying by one leaves the other side unchanged. It also explains the singleton result without a special mathematical exception. A first implementation can build separate prefix and suffix arrays, giving O(n) time and O(n) auxiliary storage. To meet the memory goal, write exclusive prefixes directly into the output, then walk backward with one suffix accumulator and multiply each output entry by that suffix.

In the forward pass, before index i, prefix is the product of nums[:i]; store it before multiplying by nums[i]. In the backward pass, before index i, suffix is the product of nums[i+1:]; combine it before incorporating nums[i]. These exclusive bounds prevent accidentally including the excluded element. The result takes O(n) arithmetic operations, O(1) auxiliary integer variables beyond the O(n) output. Python large integers occupy more than a machine word, so the strict bit-space/time cost grows with product magnitude.

Follow-up 1: work modulo a positive integer

Predict whether zero or a composite modulus breaks the two-pass method. Apply % modulus after every multiplication; associativity still holds, and no modular inverse is required.

Input [2, 3, 4], modulus 6 Ordinary output Modular output
exclude 2 12 0
exclude 3 8 2
exclude 4 6 0

Require a positive integer modulus and define modulus 1 as all zeros, including the singleton empty product. An approach based on division or inverses would fail for non-invertible entries even though the prefix/suffix method still works.

Follow-up 2: updates and repeated exclusion queries

The precomputed output becomes stale after a point update. For an associative operation, a segment tree stores interval products and updates only ancestors.

Diagram: Follow-up 2: updates and repeated exclusion queries

To exclude position 1, combine the query for [0, 1) with [2, 4); the root alone cannot answer it. Updates and one exclusion query cost O(log n), with O(n) stored state. Returning every exclusion result still costs at least O(n). A senior candidate distinguishes working space from output and derives both exclusive invariants. A lead candidate chooses between batch recomputation and an update-friendly structure using actual query/update ratios and numeric bounds.

Run and check

From the repository root:

cd curriculum/01-code/02-data-structures-algorithms/problems/07-product-except-self
python -m unittest -v test_solution.py

Reference implementation (download file, source below) · Contract and oracle tests (download file, source below). Read the tests after your attempt. A green reference suite verifies the supplied implementation; it does not demonstrate independent transfer. Reimplement one follow-up with the reference closed and explain which old invariant no longer holds.

Reference implementation · solution.py
def _integers(values):
    if not isinstance(values, (list, tuple)) or any(type(x) is not int for x in values):
        raise ValueError("expected a list or tuple of integers, excluding bool")

def product_except_self(nums):
    _integers(nums)
    result = [1] * len(nums)
    prefix = 1
    for i, value in enumerate(nums):
        result[i] = prefix
        prefix *= value
    suffix = 1
    for i in range(len(nums) - 1, -1, -1):
        result[i] *= suffix
        suffix *= nums[i]
    return result
Contract and oracle tests · test_solution.py
import itertools
import math
import unittest
from solution import product_except_self

class ProductTests(unittest.TestCase):
    def test_boundaries(self):
        self.assertEqual(product_except_self([]), [])
        self.assertEqual(product_except_self([7]), [1])
        self.assertEqual(product_except_self([0, 3, 4]), [12, 0, 0])
        self.assertEqual(product_except_self([0, 0, 4]), [0, 0, 0])
        nums = [2, 3, 4]
        self.assertEqual(product_except_self(nums), [12, 8, 6])
        self.assertEqual(nums, [2, 3, 4])
        with self.assertRaises(ValueError):
            product_except_self([False])

    def test_independent_exclusion_oracle(self):
        for n in range(6):
            for nums in itertools.product(range(-2, 3), repeat=n):
                expected = [math.prod(nums[:i] + nums[i+1:]) for i in range(n)]
                self.assertEqual(product_except_self(nums), expected)

if __name__ == "__main__":
    unittest.main()