Prefix XOR
Precompute X[i] = a[0] ^ … ^ a[i-1] so any range XOR a[l..r] is X[r+1] ^ X[l] — XOR is its own inverse, so no subtraction is needed.
Overview
Prefix XOR is Prefix Sum with ^ in place of +. Because every value is its own inverse under XOR (x ^ x = 0), the "subtract the earlier prefix" step is also an XOR: xor(l, r) = X[r+1] ^ X[l]. Building X costs O(n); each range XOR query costs O(1).
The technique combines naturally with a Hash Map to count subarrays whose XOR equals a target (X[r+1] ^ X[l] = k ⇔ X[l] = X[r+1] ^ k) and with a Trie of bit strings to find the subarray with maximum XOR. It also underlies puzzles like "find the missing number" and "single number" where XOR cancels pairs.
Intuition
A mental model before the formal terms.
A light switch flipped an even number of times is back where it started. XOR-ing a sequence of numbers is flipping a row of switches, one row per bit. To learn what the switches did between positions l and r, take the state after r, then replay the flips from before l — since replaying a flip undoes it, that is just XOR-ing with the earlier state.
How it works
- Allocate
Xof lengthn + 1withX[0] = 0. - For
ifrom1ton:X[i] = X[i-1] ^ a[i-1]. - Range query
xor(l, r): returnX[r+1] ^ X[l]. - Count subarrays with XOR
k: scan with runningX; maintainseen[value]seeded withseen[0] = 1; addseen[X ^ k]to the count at each step, thenseen[X] += 1. - Maximum subarray XOR: insert each prefix into a binary Trie; for the current prefix greedily walk the trie choosing the opposite bit at each level to maximize the result.
Why it works
Cancellation: X[r+1] ^ X[l] = (a[0] ^ … ^ a[r]) ^ (a[0] ^ … ^ a[l-1]). XOR is associative and commutative, so the terms pair up: every a[i] with i < l appears twice and vanishes (a[i] ^ a[i] = 0), leaving a[l] ^ … ^ a[r].
Counting: a subarray a[l..r] has XOR k iff X[r+1] ^ X[l] = k, and XOR-ing both sides by X[r+1] gives X[l] = X[r+1] ^ k. Every earlier prefix equal to that value is a valid left endpoint, so a hash-map lookup counts them in O(1).
The argument is identical to prefix sums; the only difference is that the inverse operation is XOR itself rather than subtraction, which removes any overflow concerns and makes the prefix array fit in the same bit width as the input.
Recognition
How to tell a problem wants this.
- "XOR of all elements between `l` and `r`", "range XOR queries", "XOR queries of a subarray".
- "Number of subarrays whose XOR equals
k", "count pairs(i, j)witha[i] ^ … ^ a[j] = 0". - "Maximum XOR of any subarray" or "maximum XOR of two numbers" — prefix XOR plus a binary trie.
- "Find the missing number in
0..n", "every element appears twice except one" — XOR cancellation without a prefix array. - Constraints stating values
< 2^20or< 2^31: a hint that bitwise structure (trie depth, mask size) matters.
Interactive visualization
Play, step, change the input. ← → and space work too.
1X[0] = 02for i in 0 .. n-1: X[i+1] = X[i] ^ a[i]3query(l, r) = X[r+1] ^ X[l]Pseudocode
1X = array of n + 1 zeros2for i in 1..n: X[i] = X[i-1] ^ a[i-1]3range_xor(l, r) = X[r+1] ^ X[l]4# count subarrays with XOR k5seen = {0: 1}, run = 0, count = 06for x in a:7 run ^= x8 count += seen[run ^ k]9 seen[run] += 110return countImplementations
1# XOR Queries of a Subarray: answer each [l, r] with a[l] ^ ... ^ a[r]2def xor_queries(a: list[int], queries: list[list[int]]) -> list[int]:31 · Prefix XOR array with sentinel X[0] = 04 x = [0] * (len(a) + 1)5 for i, v in enumerate(a):6 x[i + 1] = x[i] ^ v72 · A range XOR is two lookups: X[r+1] ^ X[l] (XOR is its own inverse)8 return [x[r + 1] ^ x[l] for l, r in queries]9 10 11# Count subarrays whose XOR equals k12def count_subarrays_with_xor(a: list[int], k: int) -> int:133 · Running prefix XOR with occurrence counts; seed the empty prefix14 seen = {0: 1}15 run = 016 count = 017 for v in a:18 run ^= v194 · Earlier prefixes equal to run ^ k close a subarray with XOR k20 count += seen.get(run ^ k, 0)215 · Record this prefix for later right endpoints22 seen[run] = seen.get(run, 0) + 123 return countenumerate(a)yields(i, v)pairs, fillingx[i + 1] = x[i] ^ vwithout manual index arithmetic.- The list comprehension answers all queries in one expression — each is two list reads and one
^. - Python ints are arbitrary precision, so
^works on values of any width with no truncation, unlike JS int32 coercion. - A plain dict with
seen.get(run ^ k, 0)reads without inserting — thedefaultdictalternative would grow on every miss. seen[run] = seen.get(run, 0) + 1records the prefix after the lookup, keepingk == 0correct.
Arbitrary-precision ints: XOR on huge values costs O(bits), still effectively O(1) for 32/64-bit inputs.
itertools.accumulate(a, operator.xor, initial=0)builds the prefix XOR array in one call (Python 3.8+).^on negative ints follows two's-complement semantics over infinite sign extension — well defined, but surprising if you expect fixed-width bits.functools.reduce(operator.xor, a, 0)is the one-shot XOR of a whole list (Single Number in one line).
- Using
defaultdict(int)and readingseen[run ^ k]— correct counts, but every miss inserts a zero entry and grows the dict. - Writing
x[r] ^ x[l](excludesa[r]) or seedingxwithout the leading 0. - Confusing
^(XOR) with**(power) — a classic slip for newcomers from math notation.
- Value width: JS/TS bitwise ops truncate to signed 32 bits (values >= 2^32 silently lose high bits; use BigInt beyond); C++
intXOR is well defined and cannot overflow; Python ints XOR at any width. - Sign surprises: JS int32 XOR can yield negative numbers when bit 31 is set (
>>> 0reinterprets as unsigned); Python treats negatives as infinitely sign-extended two's complement. - No widening needed anywhere — unlike prefix *sums*, the prefix XOR array fits the input type in every language (no
long long, no BigInt for 32-bit inputs). - Stdlib builds: Python
accumulate(a, operator.xor, initial=0); C++std::partial_sumwithstd::bit_xor<int>(); JS/TS need the manual loop. - Lookup semantics in the counting variant: C++
unordered_map::operator[]and Pythondefaultdictinsert on read (usefind/dict.get); JS/TSMap.getnever inserts.
Complexity
O(n) build, O(1) per query. Maximum-XOR-subarray with a trie is O(n · B) for B-bit values.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Range XOR queries on a static array.
- Counting or finding subarrays with a given XOR.
- Maximum subarray XOR (with a binary trie over prefixes).
- Any cumulative computation whose operation is XOR — parity tracking, toggling state.
- The array is updated between queries — a Fenwick Tree supports XOR point updates and range queries in
O(log n). - The question is about sums, counts, or ordering — XOR carries no magnitude information; use Prefix Sum.
- Range AND / OR queries — those operations are not invertible; use a Sparse Table (idempotent) or Segment Tree.
Alternatives
Common mistakes
- Writing
X[r] ^ X[l]and excludinga[r], orX[r+1] ^ X[l+1]and excludinga[l]. - Trying to "subtract" with
−out of habit; the inverse of XOR is XOR. - Forgetting
seen[0] = 1in the counting variant (misses subarrays starting at index 0). - Assuming XOR-prefix tricks extend to AND or OR — they do not, because those operations lose information.
Interview patterns
- XOR Queries of a Subarray — direct application.
- Count Triplets That Can Form Two Arrays of Equal XOR: count
(i, k)withX[i] == X[k+1], contributingk − itriplets. - Maximum XOR of Two Numbers in an Array / maximum subarray XOR with a bitwise trie.
- Single Number and Missing Number via cancellation.
- Decode XORed Array: reconstruct
afromencoded[i] = a[i] ^ a[i+1]— a prefix XOR in disguise.
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Recognizing a sliding-window problemIntermediate
- Prefix sum or segment tree?Intermediate
- Minimum Size Subarray SumIntermediate
- Subarray Sum Equals KIntermediate