algorithm
data structures
trie
XOR
programming tutorial

Find the subarray with the max XOR from an array using a trie

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The maximum XOR subarray problem asks for a contiguous segment of an array whose XOR value is as large as possible. The efficient solution uses prefix XOR values and a binary trie, reducing the problem from checking every subarray to querying the best earlier prefix for each position.

Why Prefix XOR Solves the Subarray Part

Let prefix[i] be the XOR of all elements from index 0 through index i.

Then the XOR of subarray l..r is:

prefix[r] XOR prefix[l - 1]

That is the same trick used in sum-prefix problems, except XOR replaces addition and subtraction.

So if you are at position r, the best subarray ending at r comes from finding an earlier prefix value that maximizes:

prefix[r] XOR earlier_prefix

This transforms the original problem into a repeated "find the best XOR partner" problem.

Why a Binary Trie Helps

A binary trie stores integers bit by bit. For a maximum XOR query, the greedy idea is simple:

  • if the current bit is 0, you prefer a stored 1
  • if the current bit is 1, you prefer a stored 0

That is because 1 XOR 0 = 1 and 0 XOR 1 = 1, which gives a larger contribution at that bit position.

By walking the trie from the most significant bit down to the least significant bit, you can build the maximum possible XOR against the values already inserted.

The Full Approach

The algorithm is:

  1. initialize prefix XOR to 0
  2. insert 0 into the trie before processing the array
  3. for each array element, update the running prefix XOR
  4. query the trie for the best XOR partner
  5. update the answer if this XOR is larger
  6. insert the new prefix XOR into the trie

Inserting 0 at the start is important because it allows subarrays beginning at index 0 to be considered naturally.

Python Implementation

The following code returns both the maximum XOR value and the subarray boundaries.

python
1class TrieNode:
2    def __init__(self):
3        self.child = {}
4        self.index = -1
5
6
7class BinaryTrie:
8    def __init__(self, bits=31):
9        self.root = TrieNode()
10        self.bits = bits
11
12    def insert(self, value, index):
13        node = self.root
14        for bit in range(self.bits, -1, -1):
15            current = (value >> bit) & 1
16            if current not in node.child:
17                node.child[current] = TrieNode()
18            node = node.child[current]
19        node.index = index
20
21    def query(self, value):
22        node = self.root
23        xor_value = 0
24        for bit in range(self.bits, -1, -1):
25            current = (value >> bit) & 1
26            wanted = 1 - current
27            if wanted in node.child:
28                xor_value |= (1 << bit)
29                node = node.child[wanted]
30            else:
31                node = node.child[current]
32        return xor_value, node.index
33
34
35def max_xor_subarray(arr):
36    trie = BinaryTrie()
37    trie.insert(0, -1)
38
39    prefix = 0
40    best_xor = -1
41    best_left = 0
42    best_right = 0
43
44    for i, num in enumerate(arr):
45        prefix ^= num
46        current_xor, prev_index = trie.query(prefix)
47
48        if current_xor > best_xor:
49            best_xor = current_xor
50            best_left = prev_index + 1
51            best_right = i
52
53        trie.insert(prefix, i)
54
55    return best_xor, best_left, best_right
56
57
58arr = [8, 1, 2, 12]
59print(max_xor_subarray(arr))

For this input, the best subarray is the one whose XOR value is largest among all contiguous segments.

Why the Trie Query Is Correct

The trie query is greedy from the highest bit downward. That works because a larger decision at a more significant bit always dominates any later decisions at lower bits.

For example, getting a 1 at bit position 8 is more valuable than anything you can do with bits 7 through 0. So the best strategy is always to try the opposite bit first at each level.

That is why the trie solution runs in linear time with respect to the number of array elements, assuming a fixed integer width.

Complexity

If integers are treated as fixed-width, for example 32 bits:

  • insertion is O(32)
  • query is O(32)
  • total time is O(n)
  • trie storage is O(32 * n) in the worst case

In practice, that is usually described as linear time because the bit width is constant.

Handling Negative Numbers

If the array can contain negative numbers, you must be careful because Python integers do not have fixed-width two's complement behavior by default. One common approach is to mask values to a fixed width such as 32 bits.

For many interview-style versions of the problem, the array is assumed to contain non-negative integers, which avoids that complication.

Common Pitfalls

The most common mistake is forgetting to insert the initial prefix XOR value 0. Without it, subarrays starting at index 0 are missed.

Another issue is inserting the current prefix before querying. That can let the algorithm compare the prefix against itself and accidentally consider an empty subarray.

People also often compute the maximum XOR value but forget to track which earlier prefix produced it. If you need the actual subarray boundaries, store the prefix index in the trie.

Finally, negative integers require explicit bit-width handling. The simple version of the trie solution assumes fixed-width non-negative values.

Summary

  • A subarray XOR can be written as the XOR of two prefix XOR values.
  • The problem becomes finding the best earlier prefix for each current prefix.
  • A binary trie supports fast maximum-XOR queries bit by bit.
  • Insert 0 first so subarrays starting at index 0 are included.
  • Store prefix indexes in the trie if you need the actual subarray, not just the XOR value.

Course illustration
Course illustration

All Rights Reserved.