leetcode / Arrays and hashing / Prefix sums and products core

Prefix sums and products

Spend O(n) once to store running totals, then answer any "sum of this stretch" question with one subtraction. This page builds the idea from first principles, extends it to products without division, to rectangles in a grid, and, combined with a hash map, to counting subarrays with a given sum in one pass.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The invariant
  5. Prefix sums with a hash map
  6. Products without division
  7. Two dimensions
  8. Worked example
  9. Implementation
  10. Variations and edge cases
  11. Complexity and tradeoffs
  12. Problems
  13. Medium
  14. Also uses this pattern
  15. Connections
  16. Read first
  17. Read next
  18. Check yourself
  19. References

Problem

Questions about contiguous ranges of an array ("sum from index i to j", "how many subarrays add up to k") are slow if every query re-adds its range: O(n) per query, O(n²) over many queries. Prefix sums remove the repeated addition: totals are computed once, and every range becomes the difference of two stored totals.

SignalPatternWhy
Many range-sum queries on a fixed arrayPrefix-sum arrayEach query is prefix[r + 1] − prefix[l], O(1) after O(n) setup.
"Number of subarrays with sum k" (negatives allowed)Prefix sums plus a hash mapA subarray sums to k exactly when two running sums differ by k; count earlier ones in a map.
"Equal numbers of 0s and 1s", "balanced" subarraysMap values to ±1, then find equal prefix sumsEqual running totals mean the stretch between them sums to zero.
"Product of everything except self", no divisionPrefix and suffix productsLeft product times right product covers everything but the element itself.
Sum of a sub-rectangle of a matrix, many queries2D prefix sumsFour lookups with inclusion and exclusion.
Random pick weighted by a valuePrefix sums, then binary searchPrefix totals are sorted, so a random point finds its slice by binary search.

Intuition

Think of a car's odometer. To know how far you drove between two stops you do not retrace the trip: you read the odometer at each stop and subtract. A prefix-sum array is the odometer reading after every element. Any distance between two readings is a subtraction, however long the stretch.

The same picture explains the hash-map version. A stretch sums to k when the odometer at its end is k more than at its start. So while driving, at each point ask: "how many earlier readings are exactly k below where I am now?" That is a dictionary lookup, and each such earlier reading is the start of one qualifying stretch.

A range sum as a difference of two prefix values The array 3, 1, 4, 1, 5, 9, 2, 6 drawn above its prefix array 0, 3, 4, 8, 9, 14, 23, 25, 31. The elements at indices 2 to 5 (4, 1, 5, 9) are highlighted, with sum 19. Below, the prefix entries prefix[6] = 23 and prefix[2] = 4 are highlighted; their difference is the same 19. nums 3 0 1 1 4 2 1 3 5 4 9 5 2 6 6 7 prefix 0 0 3 1 4 2 8 3 9 4 14 5 23 6 25 7 31 8 sum(nums[2..5]) = prefix[6] − prefix[2] = 23 − 4 = 19 prefix[i] holds the total of the first i elements, so prefix[0] = 0.

The extra leading zero removes the special case for ranges that start at index 0.

Mechanics

The invariant

Define prefix[0] = 0 and prefix[i] = prefix[i − 1] + nums[i − 1]. By induction, prefix[i] is the sum of the first i elements. For a range nums[l..r] inclusive, prefix[r + 1] − prefix[l] is the sum of the first r + 1 elements minus the sum of the first l elements, which leaves exactly elements l to r. The leading zero is what makes l = 0 work without a branch.

Prefix sums with a hash map

To count subarrays with sum k, you cannot precompute and query; the question is about all ranges at once. Walk the array keeping the running sum s. A subarray ending here starts after some earlier point whose prefix equals s − k. So add to the answer the number of earlier prefix values equal to s − k, then record s. Seed the map with {0: 1}, the empty prefix, so subarrays starting at index 0 count. Negative numbers are fine, because nothing relies on the prefix being monotonic (this is why a sliding window fails here).

Products without division

The product of all elements except nums[i] is (product of nums[0..i−1]) × (product of nums[i+1..n−1]). Compute the left products in one pass and the right products in a second pass. Division would fail on zeros and is usually forbidden. The right pass can multiply into the output array in place, so the extra space is O(1) beyond the answer.

Two dimensions

Let table[r][c] be the sum of the sub-grid with rows before r and columns before c. Building it uses inclusion and exclusion: a cell's table entry is the cell plus the entry above plus the entry on the left, minus the entry diagonally above-left that both of them counted. A rectangle then costs four lookups: take the big corner, subtract the strip above and the strip to the left, and add back the overlap that was subtracted twice.

A rectangle sum from four entries of a 2D prefix table A 5 by 5 prefix table for a 4 by 4 grid. The rectangle covering rows 1 to 2 and columns 1 to 2 of the grid has sum 11. It is computed as the corner entry 21, minus the entry 4 above, minus the entry 9 to the left, plus the overlap entry 3. The four entries are highlighted: green for the corner, amber for the two subtracted entries and blue for the added one. 0 0 0 0 0 0 3 3 4 8 0 8 14 18 24 0 9 17 21 28 0 13 22 26 34 rectangle = rows 1 to 2, columns 1 to 2 21 − 4 − 9 + 3 = 11 green: big corner, amber: strips to remove, blue: overlap added back. Table row 0 and column 0 are the zero padding.

Four lookups whatever the size of the rectangle.

Worked example

Range queries. On [3, 1, 4, 1, 5, 9, 2, 6] the prefix array is [0, 3, 4, 8, 9, 14, 23, 25, 31]. Each query below is two lookups and one subtraction.

Rangeprefix[r + 1] − prefix[l]Sum
[0..7]31 − 031
[2..5]23 − 419
[3..3]9 − 81
[4..6]25 − 916

Counting subarrays with sum 7 in [3, 4, 7, 2, -3, 1, 4, 2] with the map of earlier prefix sums. At each element, need is the running sum minus 7, and earlier is how many times that value appeared among earlier prefix sums (the empty prefix 0 included).

ElementRunning sumNeed (sum − 7)EarlierTotal so far
33-400
47011
714712
216902
-313602
114713
4181103
2201314

There are 4 such subarrays: [3, 4], [7], [7, 2, −3, 1] and [1, 4, 2]. The run made 8 map lookups; checking all ranges would take 36.

Running sums and the subarrays that end at each point A bar for each running sum of [3, 4, 7, 2, −3, 1, 4, 2]: 3, 7, 14, 16, 13, 14, 18, 20. Bars are highlighted where an earlier running sum exactly 7 lower exists, which happens at 4 of the 8 positions, giving 4 subarrays. 3 3 7 4 14 7 16 2 13 -3 14 1 18 4 20 2 Under each bar: the element added. Green: a start exists 7 lower; found ×4 in total.

Bar height is the running sum; green bars end at least one subarray summing to 7.

Products. For [1, 2, 3, 4]: left products [1, 1, 2, 6], right products [24, 12, 4, 1], so each position is their product: [24, 12, 8, 6].

Implementation

from itertools import accumulate


class RangeSum:                                 # immutable array, many queries
    def __init__(self, nums):
        self.prefix = [0, *accumulate(nums)]

    def query(self, left, right):               # inclusive
        return self.prefix[right + 1] - self.prefix[left]


def count_subarrays_with_sum(nums, k):          # works with negatives
    seen = {0: 1}                               # the empty prefix
    running = total = 0
    for x in nums:
        running += x
        total += seen.get(running - k, 0)
        seen[running] = seen.get(running, 0) + 1
    return total


def product_except_self(nums):
    out = [1] * len(nums)
    for i in range(1, len(nums)):               # out[i] = product of everything left of i
        out[i] = out[i - 1] * nums[i - 1]
    right = 1
    for i in range(len(nums) - 1, -1, -1):      # fold in the product of everything right of i
        out[i] *= right
        right *= nums[i]
    return out


class RangeSum2D:
    def __init__(self, grid):
        rows, cols = len(grid), len(grid[0])
        self.t = [[0] * (cols + 1) for _ in range(rows + 1)]
        for r in range(rows):
            for c in range(cols):
                self.t[r + 1][c + 1] = (grid[r][c] + self.t[r][c + 1]
                                        + self.t[r + 1][c] - self.t[r][c])

    def query(self, r1, c1, r2, c2):
        t = self.t
        return t[r2 + 1][c2 + 1] - t[r1][c2 + 1] - t[r2 + 1][c1] + t[r1][c1]

Variations and edge cases

  • Longest subarray with a property. Store the first index at which each prefix value appeared; when the value appears again, the stretch between has the property and its length is the difference of indices. Balanced 0/1 arrays use this with 0 mapped to −1.
  • Divisibility. To count subarrays whose sum is divisible by k, count prefix sums by sum % k; two equal remainders bracket a multiple of k. (Python's modulo is never negative, which helps.)
  • Trees. Running sums along a root-to-node path work the same way, but the map must be undone when the DFS leaves a node so sibling branches do not see it.
  • Weighted random pick. The prefix array is sorted, so bisect_left(prefix, random_point) chooses the slice containing the point.
  • Updates. If the array changes between queries, a prefix array costs O(n) per update; use a Fenwick or segment tree instead.
  • Edge cases: forgetting the leading zero (and the {0: 1} seed), off-by-one on inclusive ranges, recording the running sum before looking up (double counts when k = 0), integer overflow in other languages, and zeros in the array if you try division.

Complexity and tradeoffs

  • Time: O(n) to build, O(1) per range query. The map version is O(n) total. The 2D table is O(rows × cols) to build and O(1) per rectangle.
  • Space: O(n) for the table (or O(distinct prefixes) for the map version). The product trick is O(1) extra beyond the output.
  • Baselines. Re-adding each range is O(n) per query. For only one query, a plain loop is as good and uses no extra memory. A sliding window solves "sum equals k" in O(1) space but only when all values are non-negative.
  • When it fails. Mutable arrays (use a Fenwick tree), operations that are not invertible, like max or min (use a sparse table or segment tree), and products with zeros when you want to divide.

Problems

The home problems cover the three flavors: precomputed products and rectangles, running-sum maps for counting and balancing, and prefix sums as a lookup structure (weighted random, jump windows). The "also uses" list shows prefix totals inside windows, sweep lines and the trapped-water problem.

Medium

  • 238. Product of Array Except Self Blind 75 Grind 169 NeetCode 150 NeetCode 250
    The answer at i is the product of everything to its left times everything to its right, computed in two passes without division.
  • 304. Range Sum Query 2D - Immutable NeetCode 250
    Precompute 2D prefix sums once, then every rectangle's sum is four lookups combined by inclusion and exclusion.
  • 437. Path Sum III Grind 169
    Count running sums along the current root-to-node path in a map, and look up how many earlier sums differ from the current one by the target.
  • 525. Contiguous Array Grind 169
    Count 0 as -1, and then any two equal running sums mark a subarray with equal 0s and 1s.
  • 528. Random Pick with Weight Grind 169
    Build prefix sums of the weights and binary-search a random point in the total to find which index's slice it lands in.
  • 560. Subarray Sum Equals K Grind 169 NeetCode 250
    Count earlier running sums in a map, because a subarray sums to k exactly when two running sums differ by k.
  • 1871. Jump Game VII NeetCode 250
    Count how many reachable indices lie in the window [i - maxJump, i - minJump], with a sliding count or a running prefix sum.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

The hash-map flavor is the complement-map idea from the hash maps page applied to running totals. Fenwick and segment trees keep prefix sums fast under updates. Sweep lines are prefix sums over events, and 2D prefix sums reappear in matrix problems.

Check yourself

Why does the prefix array have n + 1 entries and start with 0?

So that sum(nums[l..r]) = prefix[r + 1] − prefix[l] holds for every l, including l = 0, without a special case. On the example the whole array is prefix[8] − prefix[0] = 31.

Why must the map be seeded with {0: 1} when counting subarrays with sum k?

The empty prefix has sum 0. Without it, subarrays that start at index 0 are never counted (in the trace, [3, 4] is found only because 0 is in the map).

A sliding window counts subarrays with sum k when all numbers are positive. Why does it fail with negative numbers, and what replaces it?

With negatives, extending a window can lower its sum, so no rule says when to shrink it. Prefix sums with a map do not need monotonicity and still run in O(n); the example contains −3 and gets 4.

How do you compute a rectangle sum from the 2D table, and why add one entry back?

Corner − strip above − strip to the left + overlap. The overlap (the top-left region) is inside both strips, so subtracting both removes it twice. Here 21 − 4 − 9 + 3 = 11.

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms: chapter 11 (hash tables, for the map version) and chapter 14 (augmenting data structures, for range queries under updates).
  • Sedgewick and Wayne, Algorithms: section 5.1, where key-indexed counting turns counts into cumulative positions with the same running-total idea.
  • Steven Skiena, The Algorithm Design Manual: the sections on hashing and on data structures for range queries.
  • Blelloch, "Prefix sums and their applications" (1990), the paper that treats prefix sums as a primitive of parallel algorithms.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.