Count target-sum subarrays: differences of prefixes
Constructed practice problem; no company attribution. Prerequisites: two sum and prefix sums.
Candidate brief
A ledger contains signed daily adjustments. Count every nonempty contiguous date range whose total equals a target. Refunds create negative values, and zero-value days still count. Can a window that is too large always be repaired by removing its leftmost entry?
Write this:
def subarray_sum_count(nums, target):
...
| Contract | Decision |
|---|---|
| Input | List/tuple of integers and integer target; booleans excluded |
| Output | Integer count of nonempty index ranges with the target sum |
| Boundaries | Empty input gives 0; different ranges count separately; no mutation |
| Invalid input | Invalid container, element, or target raises ValueError |
| Excluded | Empty subarrays, noncontiguous selections, fixed-width overflow |
Optional refresher · the underlying tool
A prefix sum is the total of everything seen up to this point. Two prefix totals differ by the sum of the contiguous items between them:
prefix_counts = {0: 1} # empty prefix, before any item
prefix = 0
prefix += 1
print(prefix_counts.get(prefix - 1, 0)) # 1 starting position
For [1,-1,1], target 1, the answer is 3 ranges, not 2. See the three exact ranges.
A design choice worth saying aloud
prefix_counts tracks how often each prefix total has appeared. {0: 1} counts the empty prefix so a range beginning at index 0 is not lost. Query prefix - target before incrementing the current prefix, or a zero target could accidentally count an empty range.
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: Integer count of nonempty index ranges with the target sum.
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
[1,-1,0], target0- Expected result
3
What it is testing: Overlapping ranges count separately.
02 · All zeros
- Input / starting state
[0,0], target0- Expected result
3
What it is testing: Repeated prefix sums contribute multiplicity.
03 · Empty
- Input / starting state
[], target0- Expected result
0
What it is testing: The empty subarray is excluded.
04 · Negative values
- Input / starting state
[3,-2,-1], target0- Expected result
1
What it is testing: Sliding-window monotonicity is unavailable.
05 · No match
- Input / starting state
[1,2], target9- Expected result
0
What it is testing: The result is a count, never None.
06 · Invalid/atomic
- Input / starting state
- boolean element or target
- Expected result
ValueError; input unchanged
What it is testing: Exact integer validation matters.
For each case, show which branch or state change produces that result.
subarray_sum_count([1, -1, 0], 0) == 3: ranges [0, 2), [0, 3), [2, 3).
subarray_sum_count([0, 0], 0) == 3; subarray_sum_count([], 0) == 0.
subarray_sum_count([1.5], 1) 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
Turn a range question into a historical lookup
A correct baseline starts a running sum at each left endpoint and extends right,
counting matches. This costs O(n²) time and O(1) auxiliary space. A sliding window
is not generally valid: removing a negative value increases the sum, and adding
a negative can repair a sum that was too large. Instead define prefix P[j]
as the sum of the first j values, with P[0] = 0. Subtracting cancels the
shared prefix: the sum over [i, j) is P[j] - P[i].
| Prefix position j | P[j] for [1, -1, 0] |
Need earlier P[i] | Earlier frequency | Total count |
|---|---|---|---|---|
| 0 | 0 | initialization only | seed {0: 1} |
0 |
| 1 | 1 | 1 | 0 | 0 |
| 2 | 0 | 0 | 1 | 1 |
| 3 | 0 | 0 | 2 | 3 |
Rearrange the desired equality to P[i] = P[j] - target. A frequency map,
not a set, records how many earlier endpoints qualify. Before processing prefix
j, the map counts exactly positions 0 through j-1, and the answer counts
all matches ending before j. Add the matching frequency, then insert the new
prefix. Inserting first would count an empty range whenever the target is zero.
The seeded zero represents a legitimate range beginning at input position zero;
it is not an arbitrary special case.
Each range has one unique right endpoint, so this procedure counts every valid
range exactly once. Expected time is O(n), auxiliary space O(n) for distinct
prefix sums, and output O(1) integers. Python integers avoid fixed-width overflow;
these bounds count arithmetic operations rather than bit-level cost on enormous
integers. The answer itself can be n(n+1)/2, as an all-zero input demonstrates.
Follow-up 1: count only ranges of length at most two
Predict which prefix frequencies must expire. At endpoint j, allowable starts
are max(0, j-2) through j-1; a permanent map overcounts long ranges.
j for [0, 0, 0] |
Eligible prefix positions | New matches | Running total |
|---|---|---|---|
| 1 | 0 | 1 | 1 |
| 2 | 0, 1 | 2 | 3 |
| 3 | 1, 2 | 2 | 5 |
Keep a queue of eligible prefix sums, decrementing frequencies as old positions expire. Delete zero counts. This yields expected O(n) time and O(min(n, limit)) state. Expiring a value entirely when only one of several copies leaves is wrong.
Follow-up 2: find a longest matching range
Now frequency is the wrong return information: only the earliest equal prefix maximizes length for the current endpoint. A senior candidate derives the equation and changes map values to match the requested output. A lead candidate defines integer limits, retention, and whether late ledger corrections require recomputing historical answers; this append-only method does not support edits.
Run and check
From the repository root:
cd curriculum/01-code/02-data-structures-algorithms/problems/06-subarray-sum-count
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.
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 subarray_sum_count(nums, target):
_integers(nums)
if type(target) is not int:
raise ValueError("target must be an integer")
prefix_counts = {0: 1}
prefix = total = 0
for value in nums:
prefix += value
total += prefix_counts.get(prefix - target, 0)
prefix_counts[prefix] = prefix_counts.get(prefix, 0) + 1
return total
import itertools
import unittest
from solution import subarray_sum_count
class PrefixTests(unittest.TestCase):
def test_boundaries(self):
self.assertEqual(subarray_sum_count([1, -1, 0], 0), 3)
self.assertEqual(subarray_sum_count([], 0), 0)
self.assertEqual(subarray_sum_count([0] * 100, 0), 5050)
self.assertEqual(subarray_sum_count([10**100, -(10**100)], 0), 1)
for nums, target in [([True], 1), ([1.5], 1), ([1], False), (None, 0)]:
with self.assertRaises(ValueError):
subarray_sum_count(nums, target)
def test_range_sum_oracle(self):
for n in range(6):
for nums in itertools.product(range(-1, 2), repeat=n):
for target in range(-2, 3):
expected = sum(sum(nums[i:j]) == target
for i in range(n) for j in range(i + 1, n + 1))
self.assertEqual(subarray_sum_count(nums, target), expected)
if __name__ == "__main__":
unittest.main()