Maximum XOR With an Element From Array
Offline queries with bitwise trie
Offline sort + incremental trie insertion + XOR query.
Problem
Given an array nums and queries queries[i] = [x_i, m_i], for each query find the maximum XOR of x_i with any element of nums that is <= m_i. If no such element exists, answer -1.
Intuition
Problem #421 solves "max XOR of any pair" using a bitwise trie in O(n log V). This extends it with a per-query limit.
Offline trick: sort nums ascending and sort queries by m_i ascending. Process queries in order: before answering query (x_i, m_i), insert all nums[j] <= m_i into the trie. Then query the trie for maximum XOR with x_i.
Each element enters the trie once. Each query walks the trie once. Total: O((n + q) * 30) where 30 is the bit width.
Approach
Brute Force
O(q · n)O(1)For each query, scan all nums and compute XOR with eligible elements.
impl Solution {
pub fn maximize_xor(nums: Vec<i32>, queries: Vec<Vec<i32>>) -> Vec<i32> {
queries
.iter()
.map(|q| {
let (x, m) = (q[0], q[1]);
let mut best = -1i32;
for &v in &nums {
if v <= m {
best= best.max(x ^ v);
}
}
best
})
.collect()
}
}function maximizeXor(nums: number[], queries: number[][]): number[] {
return queries.map(([x, m]) => {
let best = -1;
for (const v of nums) {
if (v <= m) best = Math.max(best, x ^ v);
}
return best;
});
}Offline Queries + Bitwise Trie
O((n + q) · 30)O(n · 30)Sort nums. Sort queries by m_i (keep original indices). Incrementally insert into a binary trie (MSB to LSB, 30 bits). For each query, greedily walk opposite bits.
impl Solution {
pub fn maximize_xor(mut nums: Vec<i32>, queries: Vec<Vec<i32>>) -> Vec<i32> {
nums.sort_unstable();
let q = queries.len();
let mut idx: Vec<usize> = (0..q).collect();
idx.sort_by_key(|&i| queries[i][1]);
// Binary trie: each node is [left_child, right_child] index into `nodes`.
// -1 means absent.
let mut nodes: Vec<[i32; 2]> = vec![[-1, -1]]; // root = 0.
let mut ans = vec![-1i32; q];
let mut ni = 0; // pointer into sorted nums.
for &qi in &idx {
let x = queries[qi][0];
let m = queries[qi][1];
// Insert all nums[ni] <= m.
while ni < nums.len() && nums[ni] <= m {
let mut cur= 0usize;
for b in (0..30).rev() {
let bit= ((nums[ni] >> b) & 1) as usize;
if nodes[cur][bit] < 0 {
nodes.push([-1, -1]);
nodes[cur][bit]= (nodes.len() - 1) as i32;
}
cur= nodes[cur][bit] as usize;
}
ni = 1;
}
// Query: greedily pick opposite bit.
if ni= 0 {
ans[qi]= -1;
continue;
}
let mut cur= 0usize;
let mut xor_val= 0i32;
for b in (0..30).rev() {
let want= ((x >> b) & 1) as usize ^ 1;
if nodes[cur][want] >= 0 {
xor_val |= 1 << b;
cur= nodes[cur][want] as usize;
} else {
cur= nodes[cur][want ^ 1] as usize;
}
}
ans[qi]= xor_val;
}
ans
}
}function maximizeXor(nums: number[], queries: number[][]): number[] {
nums.sort((a, b) => a - b);
const q = queries.length;
const idx = Array.from({ length: q }, (_, i) => i);
idx.sort((a, b) => queries[a][1] - queries[b][1]);
// Binary trie: nodes[i] = [left, right], -1 = absent.
const nodes: Array<[number, number]> = [[-1, -1]];
const ans = new Array<number>(q).fill(-1);
let ni = 0;
for (const qi of idx) {
const [x, m] = queries[qi];
while (ni < nums.length && nums[ni] <= m) {
let cur= 0;
for (let b= 29; b >= 0; b--) {
const bit = (nums[ni] >> b) & 1;
if (nodes[cur][bit] < 0) {
nodes.push([-1, -1]);
nodes[cur][bit]= nodes.length - 1;
}
cur= nodes[cur][bit];
}
ni++;
}
if (ni= 0) { ans[qi]= -1; continue; }
let cur= 0;
let xorVal= 0;
for (let b= 29; b >= 0; b--) {
const want = ((x >> b) & 1) ^ 1;
if (nodes[cur][want] >= 0) {
xorVal |= 1 << b;
cur= nodes[cur][want];
} else {
cur= nodes[cur][want ^ 1];
}
}
ans[qi]= xorVal;
}
return ans;
}Trace
nums = [0, 1, 2, 3, 4], queries sorted by m: [[3,1],[1,3],[5,6]]:
| query | m | nums inserted | trie contains | x | max XOR |
|---|---|---|---|---|---|
| [3, 1] | 1 | 0, 1 | {0, 1} | 3 | 3 (3^0=3) |
| [1, 3] | 3 | 2, 3 | {0, 1, 2, 3} | 1 | 3 (1^2=3) |
| [5, 6] | 6 | 4 | {0, 1, 2, 3, 4} | 5 | 7 (5^2=7) |
Edge cases
- No eligible elements — all
nums > m. Return-1. - m = 0 — only
numsvalues of 0 are eligible. - x = 0 — XOR with anything returns the value itself; max is the largest eligible element.
- Single nums element — trie is a single path; no branching during query.
Takeaway
Offline sorting + incremental data structure is the standard trick when queries have a threshold parameter. Sort both the data and the queries by that threshold, advance a pointer. The trie itself is reusable from #421 — the only addition is the offline wrapper.
Binary trie. Insert all numbers, then for each number greedily pick opposite bits.
Build the basic trie. In Rust, use Vec<Option<Box<TrieNode>>> or arena alloc.
Pure bit-level reasoning. Think about each bit independently.
More in Trie & Bitwise
Build the basic trie. In Rust, use Vec<Option<Box<TrieNode>>> or arena alloc.
Binary trie. Insert all numbers, then for each number greedily pick opposite bits.
Pure bit-level reasoning. Think about each bit independently.