Learn/DSA Patterns
DSA PatternsHashingmedium10 min read

Hashing for Subarray Problems

A hash map that stores prefix aggregates (sums, XORs, remainders) turns any 'count subarrays satisfying a condition' problem into a single O(n) pass. This chapter deepens pattern 1.17 with the XOR, mod-k, and longest-subarray variants — and explains the design decision of *what exactly to hash*.

#hashmap#prefix-sum#prefix-xor#subarray#counting#mod#beginner#interview
Table of contents

Before we start

Pattern 1.17 introduced the core mechanic: prefix sum + hash map = O(n) subarray counting. If you haven't read it, read it first — it has the full story, the frame-by-frame trace, and the correctness proof.

This chapter focuses on one skill that 1.17 didn't drill: deciding what to hash. The prefix sum is just one option. Depending on the problem's condition, you'll hash:

  • A running sum → for sum equals k
  • A running XOR → for XOR equals k
  • A running remainder mod k → for divisibility
  • A running balance counter → for equal 0s and 1s

By the end you will be able to:

  • Choose the right aggregate to hash for any subarray condition.
  • Write the correct need = f(prefix) query for each variant.
  • Distinguish count mode (frequency map) from longest mode (first-seen index map).

The one design question

Every problem in this family reduces to one question:

Two positions i and j define a valid subarray if aggregate[i] ○ aggregate[j] = target.
What is and what is aggregate?

Once you answer that, the code is always the same skeleton:

freq = {identity: 1}   # identity element for the aggregate operation
agg = identity
count = 0

for x in arr:
    agg = combine(agg, x)         # extend the running aggregate
    need = inverse(agg, target)   # what earlier aggregate would complete the pair?
    count += freq.get(need, 0)    # how many times has that aggregate appeared?
    freq[agg] = freq.get(agg, 0) + 1

The three variants below differ only in combine, inverse, and identity.


The turning point — the hiker's balance story

A real-life story

A hiker logs her altitude change at every checkpoint. Her running total (prefix sum) starts at 0. After each checkpoint, she records the new total.

She wants to know: in how many time windows did she gain exactly 200m?

Key insight: if the total at checkpoint 7 is 900m and the total at checkpoint 3 was 700m, then between checkpoints 3 and 7 she gained exactly 200m. She doesn't scan pairs — she just asks, for each page, "how many earlier pages showed a total that is 200 less than my current total?"

This story is about sums. But the same hiker logic applies if the notebook tracks XOR values, remainders, or character balances — just change what she writes on each page.


The one idea to remember

The entire pattern in one sentence

Store a running aggregate in a hash map as you scan; for each new value, query for the "complementary earlier aggregate" that would make the window between them satisfy the target condition — then record the current aggregate for future queries.


Variant 1 — Sum equals k (the foundation)

Condition: prefix[i] - prefix[j] = kneed: prefix[i] - k

from collections import defaultdict

def subarraySum(nums, k):
    freq = defaultdict(int)
    freq[0] = 1          # empty prefix
    prefix = 0
    count = 0
    for num in nums:
        prefix += num
        count += freq[prefix - k]
        freq[prefix] += 1
    return count

combine = +, inverse = subtract k, identity = 0.


Variant 2 — XOR equals k

Condition: prefix_xor[i] ^ prefix_xor[j] = k
XOR is self-inverse: prefix_xor[j] = prefix_xor[i] ^ kneed: prefix_xor[i] ^ k

def subarrayXOR(nums, k):
    freq = defaultdict(int)
    freq[0] = 1          # empty prefix XOR = 0
    prefix = 0
    count = 0
    for num in nums:
        prefix ^= num                  # XOR instead of add
        count += freq[prefix ^ k]      # need = prefix ^ k  (because x ^ k ^ k = x)
        freq[prefix] += 1
    return count

combine = ^, inverse = XOR with k, identity = 0.

Pause & think

Why is need = prefix ^ k for XOR, but need = prefix - k for sum? What mathematical property of XOR makes this work?

Answer

XOR is its own inverse: a ^ b ^ b = a. So if prefix_xor[i] ^ prefix_xor[j] = k, then prefix_xor[j] = prefix_xor[i] ^ k (XOR both sides by prefix_xor[i]). For sum, subtraction is the inverse of addition: prefix[i] - prefix[j] = kprefix[j] = prefix[i] - k.


Variant 3 — Sum divisible by k

Condition: (prefix[i] - prefix[j]) % k = 0prefix[i] % k = prefix[j] % k
Need: the same remainder as the current prefix.

def subarraysDivByK(nums, k):
    freq = defaultdict(int)
    freq[0] = 1
    prefix = 0
    count = 0
    for num in nums:
        prefix = (prefix + num) % k
        if prefix < 0:
            prefix += k            # Python's % is always ≥ 0, but explicit guard for clarity
        count += freq[prefix]      # need = same remainder (not prefix - k!)
        freq[prefix] += 1
    return count

combine = (sum) % k, inverse = same value, identity = 0.

The key shift: we no longer store the raw prefix sum — we store the remainder. Two positions with the same remainder define a subarray divisible by k.

Pause & think

For nums = [4, 5, 0, -2, -3, 1] with k = 5, what remainders do you see at each step? How many pairs share a remainder?

Check your trace
prefix sums:    4,  9,  9,  7,  4,  5
remainders:     4,  4,  4,  2,  4,  0

Remainder 4 appears 4 times → C(4,2) = 6 pairs (each pair of indices is a valid subarray).
Remainder 0 appears 1 time1 pair with the initial freq[0]=1.
Answer: 7  ✅ (matches LC #974)

Variant 4 — Equal 0s and 1s (LC #525)

Encoding trick: replace 0 with -1. Now "equal 0s and 1s" means "subarray sums to 0".
Run Variant 1 with k = 0.

def findMaxLength(nums):
    # encode 0 → -1, then count subarrays with sum 0
    freq = {0: 1}
    prefix = 0
    count = 0
    for num in nums:
        prefix += 1 if num == 1 else -1
        count += freq.get(prefix, 0)
        freq[prefix] = freq.get(prefix, 0) + 1
    return count

The encoding is the entire insight. Once encoded, it's identical to Variant 1 with k=0.


Count mode vs Longest mode

Everything above is count mode — use a frequency map, accumulate count += freq[need].

For longest subarray, switch to a first-seen index map and take max(length):

def longestSubarraySum(nums, k):
    first_seen = {0: -1}          # prefix sum → first index it appeared
    prefix = 0
    max_len = 0
    for i, num in enumerate(nums):
        prefix += num
        if prefix - k in first_seen:
            max_len = max(max_len, i - first_seen[prefix - k])
        if prefix not in first_seen:
            first_seen[prefix] = i     # only store FIRST occurrence
    return max_len

Key differences:

Count modeLongest mode
Map storesfrequency (int)first-seen index (int)
On hitcount += freq[need]max_len = max(max_len, i - first_seen[need])
On storealways updateonly if key not yet in map

The "only if not yet in map" rule is critical for longest: you want the earliest occurrence to maximise the subarray length.


The decision table

Problem conditionAggregateneed queryIdentity
Sum == krunning sumprefix - k0
XOR == krunning XORprefix ^ k0
Sum % k == 0running sum % kprefix % k (same value)0
Equal 0s and 1srunning sum (0→-1)prefix - 0 = prefix0
Longest with sum krunning sumprefix - k0, stored at index -1

All five share the same skeleton. The only changes are one line (combine) and one line (need).


Common traps

Watch out for these

  • Query before storing. Always count += freq[need] THEN freq[prefix] += 1. Reversing this lets the current position pair with itself.
  • Forgetting freq[0] = 1 (or first_seen = {0: -1} for longest). This handles subarrays starting at index 0. Without it you silently miss them.
  • Using count mode for longest. Frequency maps don't tell you where a prefix sum appeared — only how many times. For longest, you need the index, so switch to first_seen.
  • For mod-k: negative remainders. If a number is negative, prefix % k may be negative in some languages (not Python). Add if prefix < 0: prefix += k for safety.
  • Confusing "sum divisible by k" with "sum equals k". The need query changes from prefix - k to prefix % k (same value lookup, not subtraction).

Say it like a pro (interview one-liner)

"This is a prefix-aggregate + hash map problem. I maintain a running aggregate and store how often each value has appeared. For each new position, I query 'has the complementary aggregate appeared before?' — answering in O(1). The only design choice is the right aggregate and its inverse. O(n) time, O(n) space, one pass."


Remember this forever

Hashing for Subarray Problems

freq = {identity: 1}
agg = identity
for x in arr:
    agg = combine(agg, x)
    count += freq[need(agg, target)]   # query FIRST
    freq[agg] += 1                     # store AFTER
Conditioncombineneedidentity
sum == k+ numagg - k0
XOR == k^ numagg ^ k0
sum % k == 0(+num)%kagg (same)0

Count: frequency map · Longest: first-seen index map (store only first occurrence)


Check yourself

Why does the XOR need use `prefix ^ k` while the sum need uses `prefix - k`?

Because of each operation's inverse. For sum: a - b = kb = a - k (subtraction is addition's inverse). For XOR: a ^ b = kb = a ^ k (XOR is its own inverse — XOR both sides by a: a ^ a ^ b = a ^ kb = a ^ k).

For "longest subarray with sum k," why must you only store the FIRST occurrence of each prefix sum?

The length of the subarray ending at index i with an earlier prefix at index j is i - j. To maximise this length, you want the smallest possible j — the earliest occurrence. If you overwrote with a later occurrence, you'd shorten all future answers.

What is the encoding trick for "equal number of 0s and 1s," and why does it work?

Replace every 0 with -1. Now the sum of any subarray with equal 0s and 1s becomes exactly 0 (each +1 is cancelled by a -1). The problem reduces to "count/find subarrays with sum 0" — the standard prefix-sum + hash map pattern with k = 0.


Practice problems

ProblemDifficultyWhat to noticeLink
Subarray Sum Equals KMediumCore sum variant; init freq[0]=1LC #560
Contiguous ArrayMediumEncode 0→-1; find longest with sum 0LC #525
Subarray Sums Divisible by KMediumStore remainder; need = same remainderLC #974
Count of Subarrays with XOR kMediumReplace + with ^; need = prefix ^ kLC #1542 related
Maximum Size Subarray Sum Equals kMediumLongest mode: first-seen index, store only firstLC #325

Next up: Rolling Hash / Rabin-Karp — where a hash value slides across a string in O(1) per step, enabling pattern matching in O(n) time.