Maximum XOR With an Element From Array

Hero Image

DT

Dhaval Trivedi

Co-founder, Airtribe

Problem Overview

In this article, we will discuss a LeetCode problem that revolves around the use of the Trie data structure. The problem is titled "Maximum XOR With an Element From Array". The main goal of this problem is to find the maximum XOR of a given element with any element of an array under specific constraints.

The URL for the problem is: LeetCode Problem

Problem Description

Given an array of integers nums and an array queries where queries[i] = [x_i, m_i], the task is to find the maximum XOR value for each query with some element of nums such that the element is less than or equal to m_i. The task is to return an array of answers, where answer[i] is the maximum XOR value for the i-th query satisfying the condition.

Understanding the Problem

The main challenge of this problem is efficiently finding the maximum XOR for each query, especially for large arrays. A key observation is the binary nature of the XOR operation, which leads to the application of the Trie data structure that stores numbers in a bitwise manner.

Key DSA Concepts and Theory

Trie Data Structure

A Trie, also known as a prefix tree, is a type of search tree used to store a dynamic set of strings where the keys are usually strings. However, a Trie can also be adapted to store integers by their binary representation. This makes it possible to handle the Maximum XOR challenges efficiently by focusing on bit manipulation.

Trie Basics:

  • Node: Each node in a Trie represents a bit of the binary representation of numbers.
  • Child: A node can have 0 or 2 children corresponding to the bit value (0 or 1).
  • Path: A path from the root to a leaf node represents the full binary form of a number stored in the Trie.

XOR Basics

The XOR (exclusive OR) operation is a bitwise operation with properties that make it useful for finding differences between bit states:

  • a XOR b results in a bit set to 1 if the bits of a and b are different, otherwise 0.
  • a XOR a = 0 and a XOR 0 = a.

In this problem, we leverage these properties to choose the path in a Trie, seeking the route that maximizes the number of 1s (i.e., the maximum XOR value).

Solution Approach

Detailed Solution Steps

  1. Sort the Inputs:

    • First, sort the nums array and the queries array by m_i values to efficiently manage the upper limit constraint.
  2. Build the Trie:

    • Start with an empty Trie. For each query, insert all eligible numbers from nums into the Trie. A number is eligible if it is less than or equal to the m_i value of the query.
  3. Process Each Query:

    • For each query, insert numbers into the Trie until the current m_i allows.
    • Attempt to find the maximum XOR by navigating the Trie for each query element x_i.
    • This involves choosing the opposite bit at each level if possible (to maximize 1s), otherwise, continue with the same bit.
  4. Store Results:

    • Store the resulting maximum XOR value for each query in the results list.

Both C++ and Java implementations use this approach, utilizing efficient bit manipulations and Trie operations.

C++ Implementation

#include <vector>
#include <algorithm>

class TrieNode {
public:
    TrieNode* children[2];
    TrieNode() {
        children[0] = nullptr;
        children[1] = nullptr;
    }
};

class Solution {
    TrieNode* root;
    
public:
    Solution() {
        root = new TrieNode();
    }
    
    void insert(int num) {
        TrieNode* node = root;
        for (int i = 31; i >= 0; --i) {
            int bit = (num >> i) & 1;
            if (!node->children[bit]) {
                node->children[bit] = new TrieNode();
            }
            node = node->children[bit];
        }
    }
    
    int findMaxXOR(int num) {
        TrieNode* node = root;
        int maxXOR = 0;
        for (int i = 31; i >= 0; --i) {
            int bit = (num >> i) & 1;
            if (node->children[1 - bit]) {
                maxXOR |= (1 << i);
                node = node->children[1 - bit];
            } else {
                node = node->children[bit];
            }
        }
        return maxXOR;
    }
    
    std::vector<int> maximizeXor(std::vector<int>& nums, std::vector<std::vector<int>>& queries) {
        std::sort(nums.begin(), nums.end());
        std::vector<std::vector<int>> offlineQueries;
        for (int i = 0; i < queries.size(); ++i) {
            offlineQueries.push_back({queries[i][1], queries[i][0], i});
        }
        std::sort(offlineQueries.begin(), offlineQueries.end());
        
        int idx = 0;
        std::vector<int> result(queries.size(), -1);
        
        for (const auto& q : offlineQueries) {
            int m = q[0], x = q[1], queryIndex = q[2];
            while (idx < nums.size() && nums[idx] <= m) {
                insert(nums[idx]);
                idx++;
            }
            if (idx > 0) {
                result[queryIndex] = findMaxXOR(x);
            }
        }
        
        return result;
    }
};

Java Implementation

import java.util.Arrays;

class TrieNode {
    TrieNode[] children;
    
    public TrieNode() {
        children = new TrieNode[2];
    }
}

public class Solution {
    TrieNode root;

    public Solution() {
        root = new TrieNode();
    }

    public void insert(int num) {
        TrieNode node = root;
        for (int i = 31; i >= 0; i--) {
            int bit = (num >> i) & 1;
            if (node.children[bit] == null) {
                node.children[bit] = new TrieNode();
            }
            node = node.children[bit];
        }
    }
    
    public int findMaxXOR(int num) {
        TrieNode node = root;
        int maxXOR = 0;
        for (int i = 31; i >= 0; i--) {
            int bit = (num >> i) & 1;
            if (node.children[1 - bit] != null) {
                maxXOR |= (1 << i);
                node = node.children[1 - bit];
            } else {
                node = node.children[bit];
            }
        }
        return maxXOR;
    }
    
    public int[] maximizeXor(int[] nums, int[][] queries) {
        Arrays.sort(nums);
        int[][] offlineQueries = new int[queries.length][3];
        for (int i = 0; i < queries.length; i++) {
            offlineQueries[i][0] = queries[i][1];
            offlineQueries[i][1] = queries[i][0];
            offlineQueries[i][2] = i;
        }
        Arrays.sort(offlineQueries, (a, b) -> a[0] - b[0]);
        
        int idx = 0;
        int[] result = new int[queries.length];
        
        for (int[] q : offlineQueries) {
            int m = q[0], x = q[1], queryIndex = q[2];
            while (idx < nums.length && nums[idx] <= m) {
                insert(nums[idx++]);
            }
            if (idx > 0) {
                result[queryIndex] = findMaxXOR(x);
            } else {
                result[queryIndex] = -1;
            }
        }
        
        return result;
    }
}

Time and Space Complexity Analysis

Time Complexity

  • Sorting nums and queries involves O(n log n) and O(q log q) time where n is the length of nums and q is the length of queries.
  • Inserting a number into the Trie takes O(32) = O(1) time per number since the number of bits (32) is constant (for 32-bit integers).
  • For each query, searching through the Trie also takes O(32) = O(1) time.
  • Therefore, the overall time complexity is O((n + q) log(n + q)), mainly dominated by the sorting operations.

Space Complexity

  • The space complexity primarily stems from the space used by the Trie, which can amount to O(n * 32) in the worst case for n unique numbers inserted into the Trie.
  • Thus, the space complexity is O(n).

Common Mistakes to Avoid

  • Mismanagement of the Trie Construction: Ensure that while inserting numbers into the Trie, we respect the condition num <= m_i.
  • Sorting Queries Incorrectly: Failing to sort queries along with indexes can disrupt the mapping of results back to the respective query.
  • Incorrect Bit Shifts: Pay close attention to bit shifts (>>) and masking operations (&) while inserting or finding the maximum XOR. These are critical in ensuring correct Trie navigation.

Similar Problems on LeetCode

Here are a few similar problems that involve the Trie and similar XOR/binary operations:

  • 421. Maximum XOR of Two Numbers in an Array - Link

Additional Resources and References

The following resources can provide additional insights into the concepts and help you understand or practice more on the given topic:

By understanding the underlying mechanisms of Trie's and XOR operations, solving problems related to binary operations becomes much more intuitive and efficient.