Part V · Aggregates, Stacks and Graphs Pattern 13 4 problems

Prefix Sum and Hash Map

The sliding window dies the moment a negative number appears. This is what replaces it: precompute every running total, then let a hash map find the pair you need in one pass.

Pattern 1 ends with a warning. A window only works while the running total grows as the window widens, so one negative value breaks it. Prefix sums have no such requirement. They turn “the sum of a range” into a single subtraction, and a hash map turns “find the range I want” into a single lookup.

Contents

  1. When to use
  2. Core idea
  3. Turning a range query into a pair lookup
  4. The templates
  5. Common mistakes
  6. Subarray Sum Equals K
  7. Continuous Subarray Sum
  8. Product of Array Except Self
  9. Subarray Sums Divisible by K
  10. Recap

When to use

The trigger. A question about the aggregate of a contiguous range, where a sliding window is blocked. The two usual blockers are negative numbers and a target that is an exact match or a remainder rather than a threshold.
SignalWhat it looks like
Count, not find“how many subarrays sum to k”. A window finds one; a prefix map counts all.
Exact equality“sum equals k”, not “sum at least k”. Equality is not monotone, so shrinking is unsafe.
Negative values allowedThe constraint list says values may be negative. That single line rules out the window.
Divisibility“sum is a multiple of k”. Work with remainders instead of sums.
Many range queries“answer q queries for the sum of a[i..j]”. Build the prefix array once, then each query is O(1).
Everything except me“product of all other elements”, “sum of everything but this”. Prefix and suffix passes, no map needed.

Window or prefix sum?

Sliding windowPrefix sum plus map
NeedsNon-negative values, monotone conditionNothing. Any values.
AnswersLongest, shortest, bestCount, exists, exact match
SpaceO(1)O(n) for the map
Typical prompt“smallest subarray with sum at least k”“how many subarrays sum to exactly k”

Core idea

Define P[0] = 0 and P[i] = nums[0] + … + nums[i-1]. Then the sum of any range is a single subtraction: sum(nums[i..j]) = P[j+1] - P[i]. A range query that cost O(n) now costs O(1), after one O(n) build.
nums 3 -1 4 2 5 P 0 3 2 6 8 13 P[4] − P[1] = 8 − 3 = 5 which is −1 + 4 + 2, the blue range negatives are fine: nothing here needs monotonicity
Figure 13.1 — The prefix array is one longer than the input. That extra leading zero is what makes ranges starting at index 0 work without a special case.

Turning a range query into a pair lookup

The subtraction alone still leaves O(n²) pairs to check. The second half of the pattern removes that.

You want P[j+1] - P[i] == k. Rearrange it: P[i] == P[j+1] - k. So walk the array once keeping a running P, and at each step ask the hash map how many earlier prefixes had the value P - k. Each answer is a valid subarray ending here. One pass, O(n).

This is the same rearrangement that turns Two Sum from O(n²) into O(n). It is worth saying so out loud, because it shows you see the shared idea rather than two memorised tricks.

Seed the map with {0: 1}. That entry stands for the empty prefix, the sum before any element. Without it you miss every subarray that starts at index 0. This one line is the most common bug in the pattern, and it is invisible until a test happens to need it.

The templates

Template A — the prefix array, for repeated range queries
from itertools import accumulate


def build_prefix(nums: list[int]) -> list[int]:
    """P[i] is the sum of the first i elements. len(P) == len(nums) + 1."""
    return [0, *accumulate(nums)]


def range_sum(prefix: list[int], i: int, j: int) -> int:
    """Sum of nums[i..j] inclusive, in O(1)."""
    return prefix[j + 1] - prefix[i]

itertools.accumulate does the build in one C-level pass. Know the manual loop too, but this is what you would ship.

Template B — running prefix plus a counting map
from collections import defaultdict


def count_ranges(nums: list[int], k: int) -> int:
    """Count subarrays whose sum equals k."""
    seen: defaultdict[int, int] = defaultdict(int)
    seen[0] = 1                  # the empty prefix; without it, ranges from index 0 are lost

    running = 0
    count = 0

    for value in nums:
        running += value
        count += seen[running - k]     # look up BEFORE recording this prefix
        seen[running] += 1

    return count

Look up before you record. Recording first would let a prefix pair with itself, which is an empty subarray.

Template C — prefix and suffix passes, no map
def combine_around_each(nums: list[int]) -> list[int]:
    """For each index, combine everything to its left with everything to its right."""
    n = len(nums)
    answer = [0] * n

    running = 0
    for i in range(n):           # left to right
        answer[i] = running
        running += nums[i]

    running = 0
    for i in range(n - 1, -1, -1):   # right to left
        answer[i] += running
        running += nums[i]

    return answer

Two sweeps in opposite directions, with the answer array doubling as the scratch space. This is how you get O(1) extra space on “everything except me” problems.

Common mistakes

The problems

1. Subarray Sum Equals K Medium

Problem

Given an array of integers and a target k, return the number of contiguous subarrays whose sum equals k. Values may be negative.

Approach

Solution

from collections import defaultdict


def subarray_sum(nums: list[int], k: int) -> int:
    """Count contiguous subarrays of nums summing to exactly k.

    Args:
        nums: Integers. Negative values are allowed.
        k: The exact target sum.

    Returns:
        The number of qualifying subarrays.

    Example:
        >>> subarray_sum([1, 2, 3], 3)
        2
    """
    # prefix value -> how many times it has been seen so far
    seen: defaultdict[int, int] = defaultdict(int)
    seen[0] = 1

    running = 0
    count = 0

    for value in nums:
        running += value
        # Every earlier prefix equal to (running - k) marks the start of a
        # subarray ending here whose sum is exactly k.
        count += seen[running - k]
        seen[running] += 1

    return count

Walkthrough

nums = [1, 2, 3], k = 3:

valuerunningrunning - kseen[running-k]countseen after
11-200{0:1, 1:1}
23011{0:1, 1:1, 3:1}
36312{0:1, 1:1, 3:1, 6:1}

The two subarrays are [1, 2] and [3]. Notice the seed {0: 1} is what found [1, 2].

TimeO(n)SpaceO(n)

The sibling that wants an index, not a count

def longest_subarray_sum(nums: list[int], k: int) -> int:
    """Length of the LONGEST subarray summing to k. Store first index, not counts."""
    first_index: dict[int, int] = {0: -1}     # empty prefix ends before index 0
    running = 0
    best = 0

    for index, value in enumerate(nums):
        running += value

        if running - k in first_index:
            best = max(best, index - first_index[running - k])

        # Only record the FIRST time a prefix appears: earlier start, longer span.
        if running not in first_index:
            first_index[running] = index

    return best
Counting versus optimising changes two lines. For a count, store how many times each prefix appeared. For the longest span, store the earliest index only, and never overwrite it. Overwriting is the bug that makes the answer too small, and it passes most small tests.

Edge cases to raise

Say this out loud: “Negative values plus an exact target rules out a sliding window. I rearrange P[j] - P[i] == k into P[i] == P[j] - k, which is a hash lookup, exactly like Two Sum.”

2. Continuous Subarray Sum Medium

Problem

Return True if the array has a contiguous subarray of length at least two whose sum is a multiple of k. k is a positive integer, and a multiple includes 0 × k.

The modular insight

A range sum P[j] - P[i] is a multiple of k exactly when P[j] % k == P[i] % k. So stop storing sums and store remainders. The question becomes: have I seen this remainder before, far enough back? Two equal remainders bracket a valid subarray.

There are only k possible remainders, so on a long array a repeat is guaranteed. The whole difficulty is the length-at-least-two rule.

Approach

Solution

def check_subarray_sum(nums: list[int], k: int) -> bool:
    """True if some subarray of length >= 2 sums to a multiple of k.

    Args:
        nums: Non-negative integers.
        k: A positive divisor.

    Returns:
        True if such a subarray exists.

    Example:
        >>> check_subarray_sum([23, 2, 4, 6, 7], 6)
        True
    """
    # remainder -> earliest index where the running sum had it
    first_index: dict[int, int] = {0: -1}
    running = 0

    for index, value in enumerate(nums):
        running += value
        remainder = running % k

        if remainder not in first_index:
            first_index[remainder] = index      # earliest only, never overwrite
        elif index - first_index[remainder] >= 2:
            return True

    return False

Walkthrough

nums = [23, 2, 4, 6, 7], k = 6:

indexrunningremainderseen before?action
0235norecord 5 at 0
1251norecord 1 at 1
2295yes, at 02 - 0 = 2, long enough, return True

The subarray is [2, 4], which sums to 6.

TimeO(n)SpaceO(min(n, k))

The map holds at most k distinct remainders, which is a tighter bound than O(n) and worth stating.

Two details interviewers probe

Negative values and the modulo sign. Python’s % returns a non-negative result for a positive k, so -7 % 6 is 5, not -1. That is exactly what this algorithm needs. In C++ or Java the sign follows the dividend, and you must add k and take the modulo again. Say this; it is a free point.
Why {0: -1} and not {0: 0}. Take [6, 1] with k = 6. At index 0 the remainder is 0, already in the map at -1, and 0 - (-1) = 1, which is too short. Correct: a single 6 does not qualify. With a seed of {0: 0} the arithmetic would be wrong by one and the length test would misfire.

Edge cases to raise

Say this out loud: “A range sum is divisible by k exactly when its two endpoint prefixes share a remainder, so I store remainders instead of sums, and I keep the earliest index to maximise the span.”

3. Product of Array Except Self Medium

Problem

Return an array where answer[i] is the product of every element except nums[i]. Solve it without division and in O(n) time.

Approach

Solution

def product_except_self(nums: list[int]) -> list[int]:
    """Product of all elements except the one at each index, without division.

    Args:
        nums: Integers. Zeros are allowed.

    Returns:
        A new list where answer[i] is the product of every other element.

    Example:
        >>> product_except_self([1, 2, 3, 4])
        [24, 12, 8, 6]
    """
    n = len(nums)
    answer = [1] * n

    # Pass 1, left to right: answer[i] becomes the product of everything
    # strictly to the left of i.
    prefix = 1
    for i in range(n):
        answer[i] = prefix
        prefix *= nums[i]

    # Pass 2, right to left: multiply in the product of everything
    # strictly to the right of i.
    suffix = 1
    for i in range(n - 1, -1, -1):
        answer[i] *= suffix
        suffix *= nums[i]

    return answer

Walkthrough

nums = [1, 2, 3, 4]:

iafter pass 1 (left products)suffix at ifinal
012424
111212
2248
3616
TimeO(n)SpaceO(1) extra

The output array is not counted as extra space, by the usual convention. Say so, because the problem statement usually says so too.

Why zeros make division fail

InputCorrect answerWhat division does
[1, 2, 0, 4][0, 0, 8, 0]Divides by zero.
[0, 0, 3][0, 0, 0]Total is 0, so every entry needs a special case.

You can patch division by counting the zeros and branching on 0, 1, or more. It works and it is ugly. The two-pass version needs no cases at all, which is the real argument for it.

Edge cases to raise

Say this out loud: “Everything except me is the prefix product times the suffix product, so it is two sweeps in opposite directions. Division is banned because a zero breaks it, and the two-pass version needs no zero handling at all.”

4. Subarray Sums Divisible by K Medium

Problem

Return the number of contiguous subarrays whose sum is divisible by k. Values may be negative.

Approach

Solution

from collections import defaultdict


def subarrays_div_by_k(nums: list[int], k: int) -> int:
    """Count contiguous subarrays whose sum is divisible by k.

    Args:
        nums: Integers. Negative values are allowed.
        k: A positive divisor.

    Returns:
        The number of qualifying subarrays.

    Example:
        >>> subarrays_div_by_k([4, 5, 0, -2, -3, 1], 5)
        7
    """
    counts: defaultdict[int, int] = defaultdict(int)
    counts[0] = 1                 # the empty prefix has remainder 0

    running = 0
    total = 0

    for value in nums:
        # Reducing each step keeps the key in 0..k-1. Python's % is already
        # non-negative for a positive k, so negatives need no correction.
        running = (running + value) % k

        # Each earlier prefix with this remainder closes one valid subarray.
        total += counts[running]
        counts[running] += 1

    return total

Walkthrough

nums = [4, 5, 0, -2, -3, 1], k = 5:

valuerunning % 5counts[r] beforetotal
4400
5411
0423
-2203
-3436
1017

Answer 7. Note how a remainder seen three times contributes three subarrays at its fourth appearance. That is the counting half doing its job.

TimeO(n)SpaceO(k)

The combinatorial view

If a remainder appears m times across the prefix array, including the seeded empty prefix, then it yields m × (m-1) / 2 subarrays, one for each pair. Adding counts[running] before incrementing builds that sum incrementally: 0 then 1 then 2 and so on. Mentioning the closed form shows you see the structure, not just the loop.

Edge cases to raise

Say this out loud: “Same skeleton as counting exact sums, but the key is the remainder instead of the sum. If a remainder shows up m times, that is m choose 2 subarrays, and adding the count before incrementing builds that up as I go.”

Recap

The six things to carry forward

Where this goes next

Pattern 14, Monotonic Stack, answers a different kind of range question: not the aggregate of a range, but where the range ends. For every element, what is the next larger one, and how far away is it?


12 — Dynamic Programming 14 — Monotonic Stack