Maximum Xor with an element from an array
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array nums of non-negative integers and a list of queries, where queries[i] = [x, m]. The answer to a query is the maximum of x ^ nums[j] over all elements with nums[j] ≤ m. If every element is greater than m, the answer is -1. Return the answers in query order.
Constraints (LeetCode 1707): 1 ≤ n, q ≤ 10^5, and 0 ≤ nums[j], x, m ≤ 10^9.
Example 1
nums = [0, 1, 2, 3, 4]
queries = [[3, 1], [1, 3], [5, 6]]
answer = [3, 3, 7]
// [3, 1]: allowed {0, 1}: 3^0 = 3, 3^1 = 2 -> 3
// [1, 3]: allowed {0, 1, 2, 3}: best is 1^2 = 3 -> 3
// [5, 6]: everything allowed: best is 5^2 = 7 -> 7
Example 2
nums = [5, 2, 4, 6, 6, 3]
queries = [[12, 4], [8, 1], [6, 3]]
answer = [15, -1, 5]
// [12, 4]: allowed {2, 4, 3}: 12^3 = 15
// [8, 1]: no element is <= 1 -> -1
// [6, 3]: allowed {2, 3}: 6^3 = 5
Approach 1: Brute force
Intuition. Answer each query on its own: scan the array, skip elements above the limit, and keep the best XOR. Since x ^ v ≥ 0 for non-negative values, starting best at -1 gives the “no element allowed” answer for free.
Algorithm.
- For each query
[x, m]:best = -1. - For each
vinnumswithv ≤ m:best = max(best, x ^ v). - Store
best.
static int[] bruteForce(int[] nums, int[][] queries) {
int[] ans = new int[queries.length];
for (int q = 0; q < queries.length; q++) {
int x = queries[q][0], m = queries[q][1], best = -1;
for (int v : nums) {
if (v <= m) best = Math.max(best, v ^ x); // v ^ x >= 0, so -1 survives only if nothing qualifies
}
ans[q] = best;
}
return ans;
}
Complexity. Time O(n · q), which is 10^10 at the limits. Space O(1) beyond the output.
Approach 2: Optimal, offline queries with a growing trie
Intuition. Without the limit, each query is “best XOR partner for x”. The binary trie from the maximum XOR problem answers that in 31 steps: walk from bit 30 down and take the child with the opposite bit whenever it exists, because one higher bit is worth more than all lower bits together. The limit m is the obstacle. The trie should contain only the elements ≤ m, but a different m for every query seems to need a different trie.
The way out is to choose the order in which we answer the queries. Sort the queries by m. As m grows, the set of allowed elements only grows, never shrinks. Sort nums as well and keep a pointer: before answering a query, insert every element up to its limit. Each element enters the trie once, and each query then runs the unlimited walk on a trie holding exactly its allowed elements. Answering in a different order than asked is fine as long as each answer goes back to its original index; this is what “offline” means.
Algorithm.
- Copy and sort
nums. Sort the query indices bym. - For each query index in that order: insert
sorted[inserted++]while it is≤ m. If nothing has been inserted yet, the answer is-1. Otherwise it ismaxXorWith(root, x). - Write each answer to its original position.
The trie and its walk are the same as in the two-number problem:
static class Node {
final Node[] child = new Node[2];
}
static void insert(Node root, int v) {
Node cur = root;
for (int b = HIGH_BIT; b >= 0; b--) {
int bit = (v >> b) & 1;
if (cur.child[bit] == null) cur.child[bit] = new Node();
cur = cur.child[bit];
}
}
static int maxXorWith(Node root, int x) {
Node cur = root;
int result = 0;
for (int b = HIGH_BIT; b >= 0; b--) {
int want = 1 - ((x >> b) & 1);
if (cur.child[want] != null) {
result |= 1 << b;
cur = cur.child[want];
} else {
cur = cur.child[1 - want];
}
}
return result;
}
static int[] optimalOffline(int[] nums, int[][] queries) {
int[] sorted = nums.clone();
Arrays.sort(sorted);
Integer[] order = new Integer[queries.length];
for (int i = 0; i < order.length; i++) order[i] = i;
Arrays.sort(order, (a, b) -> Integer.compare(queries[a][1], queries[b][1])); // by limit m
Node root = new Node();
int[] ans = new int[queries.length];
int inserted = 0; // sorted[0 .. inserted - 1] are in the trie
for (int q : order) {
int x = queries[q][0], m = queries[q][1];
while (inserted < sorted.length && sorted[inserted] <= m) insert(root, sorted[inserted++]);
ans[q] = inserted == 0 ? -1 : maxXorWith(root, x);
}
return ans;
}
Dry run on Example 2. Sorted nums = [2, 3, 4, 5, 6, 6], and the queries ordered by m are [8, 1], [6, 3], [12, 4]:
| Query (x, m) | Inserted now | Trie holds | Answer |
|---|---|---|---|
| (8, 1) | none | {} | -1 |
| (6, 3) | 2, 3 | {2, 3} | 6 ^ 3 = 5 |
| (12, 4) | 4 | {2, 3, 4} | 12 ^ 3 = 15 |
Placed back in the original order, the answers are [15, -1, 5].
Complexity. Time O(n log n + q log q) for the sorts, plus O(31 · (n + q)) for all inserts and walks. Space O(31 · n) trie nodes plus O(q) for the index order.
Approach 3: Optimal online, a trie that remembers subtree minimums
Intuition. Sometimes queries must be answered in the order they arrive, for example when they come from a stream. Then we cannot sort them, and the trie must hold every element while still respecting the limit. Store in every node the smallest element in its subtree. A branch contains at least one allowed element exactly when its minimum is ≤ m. The greedy walk becomes: prefer the opposite-bit child, but only if its minimum is ≤ m; otherwise take the other child.
Why the other child is always usable: the walk keeps the invariant cur.min ≤ m. A node’s minimum is the smaller of its two children’s minimums. If the preferred child is missing or has minimum > m, the other child must hold the element that gives cur.min, so it exists and satisfies the limit. The invariant starts at the root, because we answer -1 up front when root.min > m.
Algorithm.
- Insert every element, updating
minon every node along its path, the root included. - For each query: if the array is empty or
root.min > m, answer-1. Otherwise walk from bit 30 down, preferring the opposite-bit child when it exists and itsmin ≤ m, setting the result bit when that branch is taken.
static class MinNode {
final MinNode[] child = new MinNode[2];
int min = Integer.MAX_VALUE; // smallest number stored in this subtree
}
static int[] optimalOnline(int[] nums, int[][] queries) {
MinNode root = new MinNode();
for (int v : nums) {
MinNode cur = root;
cur.min = Math.min(cur.min, v);
for (int b = HIGH_BIT; b >= 0; b--) {
int bit = (v >> b) & 1;
if (cur.child[bit] == null) cur.child[bit] = new MinNode();
cur = cur.child[bit];
cur.min = Math.min(cur.min, v);
}
}
int[] ans = new int[queries.length];
for (int q = 0; q < queries.length; q++) {
int x = queries[q][0], m = queries[q][1];
if (nums.length == 0 || root.min > m) { // every number is above the limit
ans[q] = -1;
continue;
}
MinNode cur = root; // invariant: cur.min <= m
int result = 0;
for (int b = HIGH_BIT; b >= 0; b--) {
int want = 1 - ((x >> b) & 1);
MinNode preferred = cur.child[want];
if (preferred != null && preferred.min <= m) {
result |= 1 << b;
cur = preferred;
} else {
cur = cur.child[1 - want]; // must hold the number that gave cur.min <= m
}
}
ans[q] = result;
}
return ans;
}
On Example 1, query [3, 1] with x = 011: bits 30 to 3 are 0 in every element, so the walk follows the 0-branch there. At bit 2, x has a 0 and wants a 1. The 1-branch holds {4}, but its minimum 4 > 1, so the walk rejects it and takes the 0-branch {0, 1, 2, 3} (minimum 0) without setting the bit. At bit 1, x has a 1 and wants a 0. The 0-branch holds {0, 1} with minimum 0 ≤ 1, so the walk takes it and sets the bit. At bit 0, x has a 1 and wants a 0. The 0-branch holds {0}, so the walk takes it and sets the bit. The result is 011 = 3 = 3 ^ 0. An unlimited walk would have taken 4 at bit 2 and returned 3 ^ 4 = 7, which breaks the limit.
Complexity. Time O(31 · (n + q)) with no sorting. Space O(31 · n) nodes, each with one extra int.
Edge cases and pitfalls
- No allowed element. Return
-1, and check this before walking: walking an empty trie dereferencesnull. - Keep the original indices. Sorting the queries themselves and writing answers in sorted order is the classic bug in the offline method. Sort an index array instead.
- Equal limits and duplicates. The
whileloop inserts everything≤ m, so queries with the samemand repeated values innumsneed no special care. - A limit at or above every element turns the query into the unlimited maximum-XOR walk. A limit below every element gives
-1. - The array-is-empty check in the online version.
root.minstarts atInteger.MAX_VALUE, so with an empty array andm = Integer.MAX_VALUEthe testroot.min > malone would let the walk start on an empty trie. - Comparators.
Integer.compare(a, b)is safer thana - b. Values here fit, but subtraction overflows for limits near theintrange.
Related problems
- Maximum XOR of two numbers in an array is the same trie without the limit.
- Bit PreRequisites for TRIE Problems explains why the greedy walk is correct.
- Trie Implementation and Operations is the letter-based trie this structure mirrors.
- XOR of numbers in a given range is another problem built on XOR properties.