A trie over the bits of integers, most significant first. It turns XOR problems into greedy walks, and it is one of the highest-value structures to have memorised.

Implementation

struct BinaryTrie {
    static const int B = 30;                        // bits, adjust for the value range
    vector<array<int,2>> nxt;
    vector<int> cnt;
 
    BinaryTrie() { newNode(); }
    int newNode() { nxt.push_back({-1,-1}); cnt.push_back(0); return nxt.size()-1; }
 
    void insert(int x, int delta = 1) {
        int v = 0;
        for (int b = B - 1; b >= 0; b--) {
            int c = (x >> b) & 1;
            if (nxt[v][c] == -1) nxt[v][c] = newNode();
            v = nxt[v][c];
            cnt[v] += delta;
        }
    }
    void erase(int x) { insert(x, -1); }
 
    int maxXor(int x) {                             // max of x ^ y over stored y
        int v = 0, res = 0;
        for (int b = B - 1; b >= 0; b--) {
            int c = (x >> b) & 1;
            int want = c ^ 1;                       // prefer the opposite bit
            if (nxt[v][want] != -1 && cnt[nxt[v][want]] > 0) { res |= 1 << b; v = nxt[v][want]; }
            else v = nxt[v][c];
        }
        return res;
    }
};

cnt makes deletion possible (decrement rather than unlink), which is what enables sliding-window and offline variants.

The greedy is optimal

At the most significant bit, setting it beats everything the lower bits could contribute: . So taking the opposite bit whenever a subtree exists is provably optimal — no backtracking needed.

What it answers

QueryMethod
Maximum over stored greedy opposite-bit walk
Minimum greedy same-bit walk
Count stored with walk; when ‘s bit is 1, add the whole subtree matching 0 and descend the other
Count stored with total minus the above
-th smallest value of descend using subtree counts
-th smallest stored valuesame walk with
Count values subtree counts

The classic problems

Maximum XOR subarray

. Insert prefix XORs one at a time and query the maximum against each new prefix:

BinaryTrie t; t.insert(0);
int cur = 0, best = 0;
for (int x : a) {
    cur ^= x;
    best = max(best, t.maxXor(cur));
    t.insert(cur);
}

— the standard solution, and a good template for the whole family.

Maximum XOR pair in an array

Insert everything, then query each element. .

Count subarrays with XOR in

count of subarrays with XOR , answered by the “count less than” walk; the answer is .

Maximum XOR with a range constraint ()

Walk both the query and the bound simultaneously, tracking whether the prefix is already strictly below — the same tight flag idea as digit DP.

Persistent binary trie

Make it persistent and you can answer “maximum XOR of with any element in the index range : build version after inserting the first elements, then query with subtree counts differenced between versions and .

memory, per query. This is the standard solution to a whole class of otherwise-hard problems.

Binary trie vs XOR basis

Binary trieXOR basis
Storesa multiset of valuesa linear span
Max XOR with a query
Max XOR over all subsets
Count values with a property
-th smallest✔ (over the span)
Deletionhard
Memory

They answer different questions: a trie is about the elements you inserted, a basis is about all XOR combinations of them. Problems saying “choose a subset” want the basis; problems saying “choose an element” want the trie.

See also: Trie · Linear Basis · Bit Manipulation