DSA DSA In Depth
0%
design bitwise-triexorpersistent-trie

Bitwise (0-1) Tries: Maximum XOR Subarray & Subtree Queries

7 min read Module 4.1: Tries, Exact String Matching & Multi-Pattern Automata Updated Oct 23, 2026
📄 Cheat Sheet
Bitwise (0-1) Tries: Maximum XOR of Two Numbers & Subarray XOR Queries
take U forward Bitwise (0-1) Tries: Maximum XOR of Two Numbers & Subarray XOR Queries Watch on YouTube ↗

Shows how inserting fixed-width integers from MSB to LSB into a binary trie turns maximum XOR queries into a greedy O(W) bit-flip walk.

0:00 / --:--

1. MSB-to-LSB Greedy Dominance & Dynamic Deletion via cnt[u]

Why must a Bitwise Trie for XOR maximization or order-statistic comparison be rooted at the Most Significant Bit ($W - 1$) rather than bit $0$? Because binary value order is lexicographical from MSB to LSB: every number in the 1-branch at depth $k$ is strictly greater than every number in the 0-branch, since $2^k > 2^k - 1 = \sum_{j=0}^{k-1} 2^j$.

To support dynamic insert(x) and remove(x) (such as sliding-window XOR or multiset queries), we store an integer counter cnt[u] at every trie node recording how many active numbers pass through node $u$. Instead of physically freeing nodes on remove(x), we simply decrement cnt[u]-- along $x$'s path, and treat any child $c$ with c === 0 || cnt[c] === 0 as empty!

Three Canonical Bitwise Trie Augmentations:
  1. Maximum XOR Subarray / Tree Path: Since $\bigoplus_{k=L}^{R} A[k] = P[R] \oplus P[L-1]$ where $P[i] = P[i-1] \oplus A[i]$ (with $P[0] = 0$), insert $P[0]$ first, then for each $R = 1 \dots N$, query maxXor(P[R]) and insert $P[R]$. (On a tree, replace $P[i]$ with the root-to-node path XOR!)
  2. Count of $y \in S$ with $(x \oplus y) < K$: At bit $i$, let $b_x = (x \gg i) \mathrel{\&} 1$ and $b_k = (K \gg i) \mathrel{\&} 1$. If $b_k = 1$, then any $y$ in child $b_x$ produces $0 < 1$ at bit $i$ and is guaranteed $< K$, so we add cnt[child[u][b_x]] in $O(1)$ and continue down child $b_x \oplus 1$ (where bit $i$ of $x \oplus y$ equals $1 = b_k$). If $b_k = 0$, we are forced to descend into child $b_x$.
  3. Range XOR Query $\max_{L \le i \le R} (x \oplus A[i])$ via maxIdx[u]: Insert $A[0], \dots, A[R]$ in order, updating maxIdx[u] = i along each path. A branch $c$ contains at least one element from $[L, R]$ if and only if maxIdx[c] >= L!
1. maxXor(x = 010₂ = 2) on S = {1(001), 5(101), 6(110)} 0 1 (flip!) 0 (flip!) 1 1 (flip!) R 5 Picks y = 101₂ (5) -> 010 ^ 101 = 111₂ = 7 2. countXorLess(x, K): At bit i where K_i = 1 u child[b_x] (x^y)_i = 0 < K_i child[b_x ^ 1] (x^y)_i = 1 == K_i ans += cnt[child[b_x]] in O(1); descend into child[b_x ^ 1]
flowchart TD Step["At bit i (from W-1 down to 0): bx = (x >> i) & 1, bk = (K >> i) & 1"] --> Check{"bk == 1?"} Check -- Yes --> Add["ans += cnt[ch[u][bx]] (all have bit i = 0 < 1) u = ch[u][bx ^ 1] (continue where bit i == 1)"] Check -- No --> Force["u = ch[u][bx] (must match bit i == 0)"]
Hardware & Cache Reality — Flat TypedArray Pool for 0-1 Tries: In a 30-bit Binary Trie with $N = 10^5$ integers, at most $30 N + 2 \approx 3 \times 10^6$ nodes are created. Instead of allocating JS objects with { left, right, cnt }, allocate a single Int32Array(2 * maxNodes) for transitions (ch[(u << 1) | bit]) and Int32Array(maxNodes) for cnt[u]. Because (u << 1) | 0 and (u << 1) | 1 sit in adjacent 4-byte words in the same 64-byte cache line, testing ch[(u << 1) | (b ^ 1)] and falling back to ch[(u << 1) | b] never incurs a second cache miss!
Radix Tree (rax), Succinct Trie & Bitwise 0/1 XOR Trie Maximum-XOR Visualizer
O(W) Bitwise XOR · ≤ 2K−1 Radix Nodes

Walk a Bitwise 0/1 Binary Trie greedily taking the opposite bit (1 − b) from MSB to LSB to maximize x ⊕ y, inspect Path-Compressed Radix/PATRICIA edge splits ("romane" / "romanus" / "rubens"), and examine UTF-8 / 24-byte SSO memory layouts.

Step 1 / 10
Custom Input:
Timeline

Invariant & State

Greedy MSB-First Choice: 2^b > ∑_{i=0}^{b-1} 2^i -> winning bit b dominates all lower bits combined!
Bitwise 0/1 Trie Path for Query x = 0101₂ (5) vs Set {0010₂(2), 0111₂(7), 1100₂(12)}: root → b3=1 → b2=1 → b1=0 → b0=0 (y=12) Step b=3: x_b=0 wants opposite bit 1 → took 1 (XOR bit = 1)
[0]b3: 0want=1, got=1
[1]b2: 1
[2]b1: 0
[3]b0: 1
Query x
5 (0101₂)
Bit Level b
b = 3
Chosen Path y
1___₂
Accumulated x ⊕ y
1___₂ (≥ 8)
[Step 1] MSB b=3: Query x=5 (0101₂) has bit 0. Greedy opposite bit 1 exists in Trie! Take branch 1.

2. Production Implementation: Flat-Pool Bitwise Trie (insert, remove, maxXor, countXorLess, maxSubarrayXor)

/**
 * High-Performance 30-Bit Binary Trie using a Flat TypedArray Pool
 * Supports dynamic insert, remove, maxXor(x), and countXorLess(x, K) in O(W) time.
 */
class BitwiseTrie {
  constructor(maxInsertions = 100005, bitWidth = 30) {
    this.W = bitWidth;
    const maxNodes = maxInsertions * this.W + 2;
    this.ch = new Int32Array(maxNodes * 2); // ch[(u << 1) | bit]
    this.cnt = new Int32Array(maxNodes);
    this.ptr = 1; // Node 1 is root; Node 0 is null sentinel
  }

  insert(x, delta = 1) {
    let u = 1;
    this.cnt[u] += delta;
    for (let i = this.W - 1; i >= 0; i--) {
      const b = (x >>> i) & 1;
      const edgeIdx = (u << 1) | b;
      let next = this.ch[edgeIdx];
      if (next === 0) {
        next = ++this.ptr;
        this.ch[edgeIdx] = next;
      }
      u = next;
      this.cnt[u] += delta;
    }
  }

  remove(x) {
    this.insert(x, -1);
  }

  maxXor(x) {
    let u = 1;
    if (this.cnt[u] === 0) return 0;
    let ans = 0;
    for (let i = this.W - 1; i >= 0; i--) {
      const b = (x >>> i) & 1;
      const want = b ^ 1;
      const desiredChild = this.ch[(u << 1) | want];
      if (desiredChild !== 0 && this.cnt[desiredChild] > 0) {
        ans |= (1 << i);
        u = desiredChild;
      } else {
        u = this.ch[(u << 1) | b];
      }
    }
    return ans >>> 0;
  }

  /**
   * Returns count of active elements y in the trie such that (x ^ y) < k
   */
  countXorLess(x, k) {
    let u = 1;
    let total = 0;
    for (let i = this.W - 1; i >= 0 && u !== 0; i--) {
      const bx = (x >>> i) & 1;
      const bk = (k >>> i) & 1;
      if (bk === 1) {
        const sameChild = this.ch[(u << 1) | bx];
        if (sameChild !== 0) total += this.cnt[sameChild];
        u = this.ch[(u << 1) | (bx ^ 1)];
      } else {
        u = this.ch[(u << 1) | bx];
      }
    }
    return total;
  }
}

/**
 * Maximum XOR of any contiguous subarray A[L..R] in O(N * W) time
 */
function maxXorSubarray(arr, bitWidth = 30) {
  const trie = new BitwiseTrie(arr.length + 1, bitWidth);
  trie.insert(0); // Prefix XOR P[0] = 0 so subarrays starting at index 0 are included!
  let pref = 0;
  let best = 0;
  for (let i = 0; i < arr.length; i++) {
    pref ^= arr[i];
    best = Math.max(best, trie.maxXor(pref));
    trie.insert(pref);
  }
  return best;
}

3. Complexity Matrix

Problem ($N$ integers of $W$ bits) Brute Force Bitwise (0-1) Trie Space Complexity
Max XOR Pair / Max XOR Subarray $\Theta(N^2)$ $\Theta(N \cdot W)$ $O(N \cdot W)$ integers
Count Subarrays with $\text{XOR}(L..R) < K$ $\Theta(N^2)$ $\Theta(N \cdot W)$ $O(N \cdot W)$ integers
Offline Subarray $\max_{i \in [L, R]} (x \oplus A[i])$ (maxIdx) $O(Q \cdot N)$ $O((N + Q) \cdot W)$ $O(N \cdot W)$ integers
Online Subarray $\max_{i \in [L, R]} (x \oplus A[i])$ (Persistent 0-1 Trie) $O(Q \cdot N)$ $O((N + Q) \cdot W)$ $O(N \cdot W)$ nodes
Common Pitfall — Forgetting to Insert Prefix P[0] = 0 Before the Loop in Subarray XOR Problems: Every subarray XOR $\bigoplus_{k=L}^{R} A[k]$ equals $P[R] \oplus P[L-1]$. When $L = 1$ (a subarray that starts at the very first element of the array), the required partner is $P[0] = 0$. If you forget trie.insert(0) before iterating over $R = 1 \dots N$, your algorithm will never consider subarrays starting at index $0$ (unless some later prefix happens to XOR to $0$)!
Check yourself Question 1 of 4
Concept Check Bitwise Trie Traversal Order for Maximum XOR recall

You want to find $y \in S$ maximizing $x \oplus y$ for a given $W$-bit unsigned integer $x$ using a 0-1 Bitwise Trie.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Which augmentation of a Persistent 0-1 Trie allows answering $(L, R, x)$ by traversing only a single root `root[R]` instead of subtracting counts between `root[R]` and `root[L-1]`?

Key Takeaways

A Bitwise (0-1) Trie inserts W-bit integers from MSB (bit W-1) down to LSB (bit 0) because 2^k exceeds the sum of all lower powers of two.
Maximizing x ^ y greedily chooses child (b ^ 1) at each bit i if available (cnt > 0), falling back to child b in O(W) time.
Counting y with (x ^ y) < K adds cnt[child[bx]] whenever bit i of K is 1, then descends into child[bx ^ 1] where the prefix of (x ^ y) matches K.
Subarray XOR problems reduce to Pair XOR on the prefix array P[i] = P[i-1] ^ A[i], provided P[0] = 0 is inserted into the trie first.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue