Maximum XOR With an Element From Array — Offline Binary Trie

Sanjeev SharmaSanjeev Sharma
8 min read

Advertisement

Problem Statement

LeetCode 1707 — Maximum XOR With an Element From Array | Difficulty: Hard

You are given an array nums of non-negative integers and an array queries where queries[i] = [xi, mi].

The answer to the i-th query is the maximum bitwise XOR value of xi and any element of nums that does not exceed mi. In other words, the answer is max(nums[j] XOR xi) for all j such that nums[j] <= mi.

If all elements of nums are larger than mi, the answer is -1.

Return an integer array answer where answer[i] is the answer to the i-th query.

Example:

Input:  nums = [0,1,2,3,4], queries = [[3,1],[1,3],[5,6]]
Output: [3, 3, 7]
 
Query (3,1): elements <= 1 are {0,1}. max(3^0, 3^1) = max(3,2) = 3.
Query (1,3): elements <= 3 are {0,1,2,3}. max(1^0,1^1,1^2,1^3) = max(1,0,3,2) = 3.
Query (5,6): elements <= 6 are all of nums. max(5^0..5^4) = 5^2 = 7.

Constraints:

  • 1 &lt;= nums.length, queries.length &lt;= 10^5
  • 0 &lt;= nums[i], xi, mi &lt;= 10^9

Why This Problem Matters

Maximum XOR With an Element From Array is the canonical "offline + binary XOR trie" interview problem. It is a hard variant of LeetCode 421 (Maximum XOR of Two Numbers) and shows up in Google, Codeforces problem sets, and competitive-style FAANG rounds. The problem layers two of the most powerful trie patterns:

  1. Binary trie for greedy bit-by-bit XOR maximisation.
  2. Offline processing (sort queries by their bound mi, insert elements lazily) to avoid the impossible "trie indexed by value" data structure.

Mastering this problem teaches you both the XOR-trie greedy walk and the offline sweep pattern — primitives that combine to answer entire families of bit-manipulation queries in O((n + q) times log V).

The Core Insight

Two ideas compose:

Binary trie for max XOR. Insert each integer's 32-bit binary representation from the most significant bit. To find the integer that maximises x XOR n, walk the trie greedily: at each bit, prefer the opposite bit from x if it exists (forces a 1 in the XOR result at that position); otherwise take whatever exists. This gives the optimal max-XOR partner in O(32) per query.

Offline sweep for the nums[j] &lt;= mi constraint. A naive trie cannot answer "max XOR among values ≤ mi" — the trie does not store value ranges efficiently. The trick: sort queries by mi ascending, sort nums ascending, and process queries in order. Maintain a pointer j into sorted nums. For each query (x, m), advance j inserting all nums[j] &lt;= m into the trie, then run the standard max-XOR walk.

Because both arrays are sorted, every element gets inserted at most once across all queries → total inserts O(n times 32). Total queries O(q times 32). Final complexity: O((n + q) times 32).

Visual Dry Run

nums = [0,1,2,3,4] sorted; queries = [[3,1],[1,3],[5,6]].

Sort queries by m: [(idx=0,3,1), (idx=1,1,3), (idx=2,5,6)].

Query (idx=0, x=3, m=1):
  Insert nums up to <= 1: insert 0, 1. Trie has {0,1}.
  Walk x=3 = 011: greedy → flip each bit → result 011 ^ 100 not feasible, settle on 1
  → 3 XOR 0 = 3, 3 XOR 1 = 2 → max = 3.
 
Query (idx=1, x=1, m=3):
  Insert nums up to <= 3: insert 2, 3. Trie has {0,1,2,3}.
  Walk x=1 = 001: greedy → 1 XOR 2 = 3 (best).
 
Query (idx=2, x=5, m=6):
  Insert nums up to <= 6: insert 4. Trie has {0,1,2,3,4}.
  Walk x=5 = 101: greedy → 5 XOR 2 = 7 (best).

Reorder by original index: [3, 3, 7].

The trie at the end of all queries contains every nums element. Each insert happened at most once.

Solution (Optimal) — Offline Sweep + Binary Trie

Python

class Solution:
    def maximizeXor(self, nums: list[int], queries: list[list[int]]) -> list[int]:
        nums.sort()
        # Pair each query with its original index, sort by mi.
        sorted_q = sorted(((i, x, m) for i, (x, m) in enumerate(queries)),
                          key=lambda t: t[2])
        BITS = 30   # values <= 10^9 fit in 30 bits
 
        root = {}
        def insert(n: int):
            node = root
            for b in range(BITS, -1, -1):
                bit = (n >> b) & 1
                if bit not in node:
                    node[bit] = {}
                node = node[bit]
 
        def max_xor(x: int) -> int:
            if not root: return -1
            node = root
            xr = 0
            for b in range(BITS, -1, -1):
                bit = (x >> b) & 1
                want = 1 - bit
                if want in node:
                    xr = (xr << 1) | 1
                    node = node[want]
                else:
                    xr <<= 1
                    node = node[bit]
            return xr
 
        ans = [0] * len(queries)
        j = 0
        for orig_idx, x, m in sorted_q:
            while j < len(nums) and nums[j] <= m:
                insert(nums[j])
                j += 1
            ans[orig_idx] = max_xor(x) if root else -1
        return ans

JavaScript

var maximizeXor = function(nums, queries) {
  nums.sort((a, b) => a - b);
  const sortedQ = queries
    .map((q, i) => [i, q[0], q[1]])
    .sort((a, b) => a[2] - b[2]);
  const BITS = 30;
  const root = {};
 
  const insert = (n) => {
    let node = root;
    for (let b = BITS; b >= 0; b--) {
      const bit = (n >> b) & 1;
      if (!node[bit]) node[bit] = {};
      node = node[bit];
    }
  };
  const maxXor = (x) => {
    if (Object.keys(root).length === 0) return -1;
    let node = root, xr = 0;
    for (let b = BITS; b >= 0; b--) {
      const bit = (x >> b) & 1;
      const want = 1 - bit;
      if (node[want] !== undefined) {
        xr = (xr << 1) | 1;
        node = node[want];
      } else {
        xr <<= 1;
        node = node[bit];
      }
    }
    return xr;
  };
 
  const ans = new Array(queries.length).fill(0);
  let j = 0;
  for (const [origIdx, x, m] of sortedQ) {
    while (j < nums.length && nums[j] <= m) {
      insert(nums[j]);
      j++;
    }
    ans[origIdx] = Object.keys(root).length ? maxXor(x) : -1;
  }
  return ans;
};

Complexity

  • Time: O((n + q) times 32) — each element inserted once across all queries, each query walks 32 bits.
  • Space: O(n times 32) for trie nodes in the worst case.

Common Mistakes

  1. Inserting all of nums first, then querying — wrong; the trie cannot filter by nums[j] &lt;= mi after the fact.
  2. Forgetting to track original query order — sorted queries return answers in sorted-mi order; you must remap to original indices.
  3. Iterating bits low-to-high — the greedy max-XOR walk requires high-to-low (most significant bit first).
  4. Returning 0 instead of -1 when trie is empty — empty trie means no element ≤ mi, the spec says return -1.
  5. Using too few bits (e.g., 20) — values up to 10^9 need 30 bits; off-by-one here causes wrong answers.
  6. Re-sorting nums or queries inside the query loop — kills the O((n+q) times 32) guarantee.

Interview Tips

  • Begin with the unbounded variant (LeetCode 421): "Without mi, this is a plain XOR trie; answer in O(32) per query."
  • Introduce the bound: "The challenge is the nums[j] &lt;= mi filter. We cannot prune the trie after building it."
  • Pitch the offline sweep: "Sort queries by mi, sort nums, sweep the pointer."
  • State the invariant clearly: "After each query, the trie contains exactly the nums values ≤ current mi."
  • Emphasise that each element gets inserted at most once — total inserts O(n times 32).

Follow-up Questions

  • Online queries (cannot reorder)? Use a persistent trie indexed by sorted nums, or a wavelet tree.
  • Find min XOR instead of max? Same trie; greedy walk picks the same bit at each level instead of the opposite.
  • Range query: max XOR with nums[j] in [lo, hi]? Persistent trie with version per element, query the difference.
  • Numbers up to 10^18? Increase BITS to 60; algorithm and complexity scale linearly with bit-width.
  • What if duplicates in nums? No effect — duplicates may be inserted multiple times harmlessly, or skipped via a seen set.

Key Takeaways

  • Max XOR with bounded element = binary trie + offline sort-by-bound sweep.
  • Sort queries by mi, sort nums; advance a pointer inserting elements before answering each query.
  • Greedy bit-by-bit walk: at each MSB-first step, prefer the opposite bit to maximise the XOR.
  • Total complexity O((n + q) times 32); each element inserted at most once.
  • Map sorted-query results back to original indices before returning.
  • The offline-trie sweep pattern reappears in persistent tries, range XOR queries, and competitive bit-manipulation problems at FAANG and Codeforces.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading