Facebook Pixel

1803. Count Pairs With XOR in a Range

HardBit ManipulationTrieArray
Leetcode Link

Problem Description

You are given a 0-indexed integer array nums and two integers low and high. Your task is to find the number of nice pairs in the array.

A nice pair is defined as a pair of indices (i, j) where:

  • 0 <= i < j < nums.length (i.e., i comes before j in the array)
  • The XOR operation between nums[i] and nums[j] produces a result that falls within the range [low, high], inclusive
  • In other words: low <= (nums[i] XOR nums[j]) <= high

The XOR operation is a bitwise operation where each bit in the result is 1 if the corresponding bits in the two operands are different, and 0 if they are the same.

For example, if nums = [1, 4, 2, 7], low = 2, and high = 6:

  • Pair (0, 1): 1 XOR 4 = 5, which is in range [2, 6] βœ“
  • Pair (0, 2): 1 XOR 2 = 3, which is in range [2, 6] βœ“
  • Pair (0, 3): 1 XOR 7 = 6, which is in range [2, 6] βœ“
  • Pair (1, 2): 4 XOR 2 = 6, which is in range [2, 6] βœ“
  • Pair (1, 3): 4 XOR 7 = 3, which is in range [2, 6] βœ“
  • Pair (2, 3): 2 XOR 7 = 5, which is in range [2, 6] βœ“

All 6 pairs satisfy the condition, so the answer would be 6.

The challenge is to efficiently count all such pairs without checking every possible combination, especially when dealing with large arrays.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to count pairs whose XOR values fall within a range [low, high], we can transform this into a simpler problem. Instead of directly counting pairs in a range, we can use the principle: count of pairs in [low, high] = count of pairs in [0, high] - count of pairs in [0, low-1].

This transformation is powerful because counting pairs with XOR less than a certain value is easier to handle systematically than counting within a range.

For XOR operations on arrays, a Trie (prefix tree) data structure is particularly effective. Why? Because XOR operates on bits, and a Trie can efficiently represent binary numbers bit by bit. We can build a binary Trie (0-1 Trie) where each path from root to leaf represents a number in binary form.

The key insight is that as we traverse the array, for each number we encounter, we want to find how many previously seen numbers would produce an XOR result less than our limit. By storing numbers in the Trie as we go, we can efficiently query this information.

When searching for pairs with XOR less than a limit, we traverse the Trie bit by bit from the most significant bit. At each bit position:

  • If the limit's current bit is 1, we have flexibility. We can take all numbers that would give us a 0 at this position (making the XOR result definitely smaller), and then continue exploring the path that gives us a 1.
  • If the limit's current bit is 0, we must follow the path that keeps our XOR result's bit as 0 to stay under the limit.

By processing numbers one by one and querying the Trie before inserting each number, we ensure we only count each pair once (since we only look at previously inserted numbers). This approach gives us an efficient O(n * log(max_value)) solution where n is the array length and log(max_value) represents the number of bits we need to check.

Learn more about Trie patterns.

Solution Approach

The solution uses a 0-1 Trie (binary Trie) to efficiently count pairs with XOR values in the range [low, high]. Let's walk through the implementation step by step.

Trie Node Structure

The Trie node contains:

  • children[0] and children[1]: pointers to left (bit 0) and right (bit 1) child nodes
  • cnt: counter tracking how many numbers pass through or end at this node

Insert Function

The insert(x) function adds a number to the Trie:

  1. Start from the root node
  2. Process bits from most significant (bit 15) to least significant (bit 0)
  3. For each bit position i:
    • Extract the bit value: v = x >> i & 1
    • If the child node for this bit doesn't exist, create it
    • Move to that child node and increment its counter

Search Function

The search(x, limit) function counts how many numbers in the Trie have XOR with x less than limit:

  1. Start from the root with ans = 0
  2. For each bit position from 15 to 0:
    • Get current bit of x: v = x >> i & 1
    • Check the current bit of limit: limit >> i & 1
    If limit's bit is 1:
    • We can take all numbers that would make XOR bit = 0 (same as x's bit)
    • Add node.children[v].cnt to answer (all these paths give XOR < limit)
    • Continue exploring the path where XOR bit = 1: node = node.children[v ^ 1]
    If limit's bit is 0:
    • We must follow the path where XOR bit = 0 to stay under limit
    • Move to: node = node.children[v] (same bit as x)

Main Algorithm

The countPairs function orchestrates the solution:

  1. Initialize answer counter and empty Trie
  2. For each number x in nums:
    • Calculate pairs with XOR in [0, high]: tree.search(x, high + 1)
    • Subtract pairs with XOR in [0, low-1]: tree.search(x, low)
    • The difference gives pairs with XOR in [low, high]
    • Insert x into the Trie for future queries
  3. Return the total count

Why This Works

By processing numbers sequentially and querying before inserting, we ensure:

  • Each pair (i, j) is counted exactly once when processing nums[j]
  • The Trie contains all numbers with indices less than current index
  • The bit-by-bit comparison efficiently filters numbers based on XOR constraints

The time complexity is O(n * 16) where n is the array length and 16 is the number of bits processed (for numbers up to 2^16). The space complexity is O(n * 16) for storing the Trie.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with nums = [3, 10, 5, 25], low = 5, and high = 15.

Step 1: Process nums[0] = 3 (binary: 0011)

  • Query: How many numbers already in Trie have XOR with 3 in range [5, 15]?
  • Trie is empty, so count = 0
  • Insert 3 into Trie

Step 2: Process nums[1] = 10 (binary: 1010)

  • Query: How many numbers in Trie have XOR with 10 in range [5, 15]?
  • We need to check: 3 XOR 10 = 9 (binary: 1001)
  • Is 9 in [5, 15]? Yes! Count = 1
  • Insert 10 into Trie

Let's trace the search process for search(10, 16) - search(10, 5):

  • For search(10, 16) with limit = 16 (binary: 10000):
    • Starting from MSB, we traverse the Trie comparing bits
    • At each level, we check if we can count paths or need to continue
    • Result: 1 (the path representing 3)
  • For search(10, 5) with limit = 5 (binary: 00101):
    • Similar traversal but with stricter limit
    • Result: 0 (3 XOR 10 = 9, which is β‰₯ 5)
  • Difference: 1 - 0 = 1 valid pair

Step 3: Process nums[2] = 5 (binary: 0101)

  • Query: How many numbers in Trie have XOR with 5 in range [5, 15]?
  • Check: 3 XOR 5 = 6 (in range βœ“), 10 XOR 5 = 15 (in range βœ“)
  • Count = 2
  • Insert 5 into Trie

Step 4: Process nums[3] = 25 (binary: 11001)

  • Query: How many numbers in Trie have XOR with 25 in range [5, 15]?
  • Check: 3 XOR 25 = 26 (out of range βœ—), 10 XOR 25 = 19 (out of range βœ—), 5 XOR 25 = 28 (out of range βœ—)
  • Count = 0
  • Insert 25 into Trie

Final Answer: 0 + 1 + 2 + 0 = 3 nice pairs

The pairs are: (0,1) with XOR=9, (0,2) with XOR=6, and (1,2) with XOR=15.

Solution Implementation

1from typing import List
2
3
4class Trie:
5    """Binary trie node for storing numbers in binary representation."""
6  
7    def __init__(self):
8        # children[0] represents bit 0, children[1] represents bit 1
9        self.children = [None] * 2
10        # Count of numbers passing through this node
11        self.cnt = 0
12  
13    def insert(self, x: int) -> None:
14        """Insert a number into the trie by its binary representation.
15      
16        Args:
17            x: The number to insert into the trie.
18        """
19        node = self
20        # Process bits from most significant (bit 15) to least significant (bit 0)
21        for i in range(15, -1, -1):
22            # Extract the i-th bit of x
23            bit = (x >> i) & 1
24          
25            # Create new node if path doesn't exist
26            if node.children[bit] is None:
27                node.children[bit] = Trie()
28          
29            # Move to the child node
30            node = node.children[bit]
31            # Increment count for this path
32            node.cnt += 1
33  
34    def search(self, x: int, limit: int) -> int:
35        """Count numbers in trie where XOR with x is less than limit.
36      
37        Args:
38            x: The number to XOR with existing numbers in trie.
39            limit: The upper bound (exclusive) for XOR result.
40          
41        Returns:
42            Count of numbers where (number XOR x) < limit.
43        """
44        node = self
45        count = 0
46      
47        # Process bits from most significant to least significant
48        for i in range(15, -1, -1):
49            if node is None:
50                return count
51          
52            # Extract the i-th bit of x and limit
53            x_bit = (x >> i) & 1
54            limit_bit = (limit >> i) & 1
55          
56            if limit_bit == 1:
57                # If limit bit is 1, we can take all numbers with same bit as x
58                # (which would give XOR bit = 0, making result smaller)
59                if node.children[x_bit] is not None:
60                    count += node.children[x_bit].cnt
61              
62                # Continue searching with opposite bit (XOR bit = 1)
63                node = node.children[x_bit ^ 1]
64            else:
65                # If limit bit is 0, we must take same bit to keep XOR result small
66                node = node.children[x_bit]
67      
68        return count
69
70
71class Solution:
72    def countPairs(self, nums: List[int], low: int, high: int) -> int:
73        """Count pairs (i, j) where i < j and low <= nums[i] XOR nums[j] <= high.
74      
75        Args:
76            nums: List of integers.
77            low: Lower bound (inclusive) for XOR value.
78            high: Upper bound (inclusive) for XOR value.
79          
80        Returns:
81            Number of valid pairs.
82        """
83        pair_count = 0
84        trie = Trie()
85      
86        # For each number, count valid pairs with previously seen numbers
87        for num in nums:
88            # Count pairs where XOR is in range [low, high]
89            # This equals: (pairs with XOR < high+1) - (pairs with XOR < low)
90            pairs_below_high = trie.search(num, high + 1)
91            pairs_below_low = trie.search(num, low)
92            pair_count += pairs_below_high - pairs_below_low
93          
94            # Insert current number for future pairs
95            trie.insert(num)
96      
97        return pair_count
98
1/**
2 * Trie data structure for storing binary representations of integers
3 * Used to efficiently count pairs with XOR values within a given range
4 */
5class Trie {
6    // Array to store children nodes (0 or 1 for binary representation)
7    private Trie[] children = new Trie[2];
8    // Count of numbers that pass through this node
9    private int count;
10
11    /**
12     * Inserts a number into the trie by its binary representation
13     * @param number The integer to insert into the trie
14     */
15    public void insert(int number) {
16        Trie currentNode = this;
17      
18        // Process each bit from most significant to least significant (15th to 0th bit)
19        for (int bitPosition = 15; bitPosition >= 0; bitPosition--) {
20            // Extract the bit at current position
21            int bitValue = (number >> bitPosition) & 1;
22          
23            // Create new node if path doesn't exist
24            if (currentNode.children[bitValue] == null) {
25                currentNode.children[bitValue] = new Trie();
26            }
27          
28            // Move to the child node
29            currentNode = currentNode.children[bitValue];
30            // Increment count for this path
31            currentNode.count++;
32        }
33    }
34
35    /**
36     * Searches for count of numbers whose XOR with given number is less than limit
37     * @param number The number to XOR with existing numbers in trie
38     * @param limit The upper bound (exclusive) for XOR result
39     * @return Count of numbers whose XOR with given number is less than limit
40     */
41    public int search(int number, int limit) {
42        Trie currentNode = this;
43        int pairCount = 0;
44      
45        // Process each bit from most significant to least significant
46        for (int bitPosition = 15; bitPosition >= 0 && currentNode != null; bitPosition--) {
47            // Extract the bit of number at current position
48            int numberBit = (number >> bitPosition) & 1;
49          
50            // Extract the bit of limit at current position
51            if (((limit >> bitPosition) & 1) == 1) {
52                // If limit bit is 1, we can include all numbers with XOR bit = 0
53                // (since they will be smaller than limit at this position)
54                if (currentNode.children[numberBit] != null) {
55                    pairCount += currentNode.children[numberBit].count;
56                }
57                // Continue searching with XOR bit = 1 (equal to limit bit)
58                currentNode = currentNode.children[numberBit ^ 1];
59            } else {
60                // If limit bit is 0, we must have XOR bit = 0 to stay under limit
61                // This means we need to follow the same bit as number
62                currentNode = currentNode.children[numberBit];
63            }
64        }
65      
66        return pairCount;
67    }
68}
69
70/**
71 * Solution class for counting pairs with XOR in given range
72 */
73class Solution {
74    /**
75     * Counts pairs of numbers whose XOR is within [low, high] range
76     * @param nums Array of integers
77     * @param low Lower bound (inclusive) for XOR value
78     * @param high Upper bound (inclusive) for XOR value
79     * @return Count of pairs (i, j) where i < j and low <= nums[i] XOR nums[j] <= high
80     */
81    public int countPairs(int[] nums, int low, int high) {
82        Trie trie = new Trie();
83        int totalPairs = 0;
84      
85        // Process each number in the array
86        for (int currentNumber : nums) {
87            // Count pairs with XOR < (high + 1) minus pairs with XOR < low
88            // This gives pairs with XOR in range [low, high]
89            totalPairs += trie.search(currentNumber, high + 1) - trie.search(currentNumber, low);
90          
91            // Insert current number into trie for future pair calculations
92            trie.insert(currentNumber);
93        }
94      
95        return totalPairs;
96    }
97}
98
1class Trie {
2public:
3    // Constructor initializes a trie node with 2 children (for binary 0 and 1)
4    Trie() : children(2), count(0) {}
5
6    // Insert a number into the trie by its binary representation
7    void insert(int number) {
8        Trie* currentNode = this;
9      
10        // Process each bit from most significant (bit 15) to least significant (bit 0)
11        for (int bitPosition = 15; bitPosition >= 0; --bitPosition) {
12            // Extract the bit at current position (0 or 1)
13            int bitValue = (number >> bitPosition) & 1;
14          
15            // Create new node if path doesn't exist
16            if (!currentNode->children[bitValue]) {
17                currentNode->children[bitValue] = new Trie();
18            }
19          
20            // Move to the child node
21            currentNode = currentNode->children[bitValue];
22            // Increment count of numbers passing through this node
23            ++currentNode->count;
24        }
25    }
26
27    // Count how many numbers in trie have XOR with given number less than limit
28    int search(int number, int xorLimit) {
29        Trie* currentNode = this;
30        int pairCount = 0;
31      
32        // Traverse the trie bit by bit from MSB to LSB
33        for (int bitPosition = 15; bitPosition >= 0 && currentNode; --bitPosition) {
34            // Get current bit of the input number
35            int numberBit = (number >> bitPosition) & 1;
36          
37            // Get current bit of the limit
38            if ((xorLimit >> bitPosition) & 1) {
39                // If limit bit is 1, we can take all numbers going through 
40                // the path that gives XOR bit = 0 (same bit values)
41                if (currentNode->children[numberBit]) {
42                    pairCount += currentNode->children[numberBit]->count;
43                }
44                // Continue search with XOR bit = 1 (opposite bit values)
45                currentNode = currentNode->children[numberBit ^ 1];
46            } else {
47                // If limit bit is 0, we must follow the path that gives
48                // XOR bit = 0 to stay under the limit
49                currentNode = currentNode->children[numberBit];
50            }
51        }
52      
53        return pairCount;
54    }
55
56private:
57    vector<Trie*> children;  // Pointers to child nodes (index 0 for bit 0, index 1 for bit 1)
58    int count;               // Number of integers that pass through this node
59};
60
61class Solution {
62public:
63    // Count pairs (i, j) where i < j and low <= nums[i] XOR nums[j] <= high
64    int countPairs(vector<int>& nums, int low, int high) {
65        Trie* trieRoot = new Trie();
66        int totalPairs = 0;
67      
68        // Process each number in the array
69        for (int& currentNum : nums) {
70            // Count pairs with XOR in range [low, high]
71            // This is done by counting pairs with XOR < (high + 1) 
72            // and subtracting pairs with XOR < low
73            int pairsLessThanHighPlusOne = trieRoot->search(currentNum, high + 1);
74            int pairsLessThanLow = trieRoot->search(currentNum, low);
75            totalPairs += pairsLessThanHighPlusOne - pairsLessThanLow;
76          
77            // Insert current number into trie for future pairs
78            trieRoot->insert(currentNum);
79        }
80      
81        return totalPairs;
82    }
83};
84
1// Trie node structure for storing binary representations
2interface TrieNode {
3    children: (TrieNode | null)[];  // Array of 2 children (for binary 0 and 1)
4    count: number;                    // Number of integers that pass through this node
5}
6
7// Create a new trie node with 2 children slots (for binary 0 and 1)
8function createTrieNode(): TrieNode {
9    return {
10        children: [null, null],
11        count: 0
12    };
13}
14
15// Insert a number into the trie by its binary representation
16function insert(root: TrieNode, number: number): void {
17    let currentNode = root;
18  
19    // Process each bit from most significant (bit 15) to least significant (bit 0)
20    for (let bitPosition = 15; bitPosition >= 0; bitPosition--) {
21        // Extract the bit at current position (0 or 1)
22        const bitValue = (number >> bitPosition) & 1;
23      
24        // Create new node if path doesn't exist
25        if (!currentNode.children[bitValue]) {
26            currentNode.children[bitValue] = createTrieNode();
27        }
28      
29        // Move to the child node
30        currentNode = currentNode.children[bitValue]!;
31        // Increment count of numbers passing through this node
32        currentNode.count++;
33    }
34}
35
36// Count how many numbers in trie have XOR with given number less than limit
37function search(root: TrieNode, number: number, xorLimit: number): number {
38    let currentNode: TrieNode | null = root;
39    let pairCount = 0;
40  
41    // Traverse the trie bit by bit from MSB to LSB
42    for (let bitPosition = 15; bitPosition >= 0 && currentNode; bitPosition--) {
43        // Get current bit of the input number
44        const numberBit = (number >> bitPosition) & 1;
45      
46        // Get current bit of the limit
47        if ((xorLimit >> bitPosition) & 1) {
48            // If limit bit is 1, we can take all numbers going through 
49            // the path that gives XOR bit = 0 (same bit values)
50            if (currentNode.children[numberBit]) {
51                pairCount += currentNode.children[numberBit]!.count;
52            }
53            // Continue search with XOR bit = 1 (opposite bit values)
54            currentNode = currentNode.children[numberBit ^ 1];
55        } else {
56            // If limit bit is 0, we must follow the path that gives
57            // XOR bit = 0 to stay under the limit
58            currentNode = currentNode.children[numberBit];
59        }
60    }
61  
62    return pairCount;
63}
64
65// Count pairs (i, j) where i < j and low <= nums[i] XOR nums[j] <= high
66function countPairs(nums: number[], low: number, high: number): number {
67    const trieRoot = createTrieNode();
68    let totalPairs = 0;
69  
70    // Process each number in the array
71    for (const currentNum of nums) {
72        // Count pairs with XOR in range [low, high]
73        // This is done by counting pairs with XOR < (high + 1) 
74        // and subtracting pairs with XOR < low
75        const pairsLessThanHighPlusOne = search(trieRoot, currentNum, high + 1);
76        const pairsLessThanLow = search(trieRoot, currentNum, low);
77        totalPairs += pairsLessThanHighPlusOne - pairsLessThanLow;
78      
79        // Insert current number into trie for future pairs
80        insert(trieRoot, currentNum);
81    }
82  
83    return totalPairs;
84}
85

Time and Space Complexity

Time Complexity: O(n Γ— log M)

The algorithm iterates through each element in the nums array once. For each element x:

  • It performs two search operations: tree.search(x, high + 1) and tree.search(x, low). Each search traverses the Trie from root to leaf, examining exactly 16 bits (since we iterate from bit 15 down to bit 0), taking O(log M) time where log M = 16.
  • It performs one insert operation: tree.insert(x). The insert operation also traverses 16 bits to insert the number into the Trie, taking O(log M) time.

Since we process n elements and each element requires O(log M) operations, the total time complexity is O(n Γ— log M), where n is the length of the array nums and M is the maximum possible value (here log M = 16 for 16-bit integers).

Space Complexity: O(n Γ— log M)

The space is primarily consumed by the Trie structure:

  • In the worst case, each number in nums creates a unique path in the Trie from root to leaf.
  • Each path consists of 16 nodes (one for each bit position from 15 to 0).
  • With n numbers, the maximum number of nodes created is O(n Γ— 16) = O(n Γ— log M).
  • Each Trie node contains an array of size 2 for children and an integer counter, which is O(1) space per node.

Therefore, the total space complexity is O(n Γ— log M), where log M = 16 represents the bit length of the numbers being processed.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Bit Range Assumption

Pitfall: The code assumes all numbers fit within 16 bits (processes bits 15 down to 0). If the input contains numbers larger than 2^16 - 1 (65535), the solution will produce incorrect results because higher-order bits are ignored.

Example of Failure:

nums = [100000, 100001, 2]  # 100000 requires at least 17 bits
low = 1
high = 3
# The code would incorrectly handle the XOR of 100000 and 100001

Solution: Determine the actual bit range needed:

def countPairs(self, nums: List[int], low: int, high: int) -> int:
    # Find the maximum value to determine bit range
    max_val = max(max(nums), high) if nums else 0
    bit_length = max_val.bit_length() if max_val > 0 else 1
  
    # Then modify insert and search to use bit_length instead of hardcoded 15
    # In insert and search methods:
    for i in range(bit_length - 1, -1, -1):  # Instead of range(15, -1, -1)

2. Null Node Handling in Search

Pitfall: The search function checks if node is None inside the loop, but if a child node doesn't exist when trying to follow a path, the code will crash with an AttributeError when trying to access node.children.

Example of Failure:

# If the trie is sparse and certain paths don't exist
# node = node.children[x_bit ^ 1]  # This could set node to None
# Then on next iteration: node.children[x_bit]  # AttributeError!

Solution: Add proper null checks before accessing children:

def search(self, x: int, limit: int) -> int:
    node = self
    count = 0
  
    for i in range(15, -1, -1):
        if node is None:
            return count
      
        x_bit = (x >> i) & 1
        limit_bit = (limit >> i) & 1
      
        if limit_bit == 1:
            # Check if child exists before accessing cnt
            if node.children[x_bit] is not None:
                count += node.children[x_bit].cnt
          
            # Check if the path we want to follow exists
            if node.children[x_bit ^ 1] is not None:
                node = node.children[x_bit ^ 1]
            else:
                return count  # No more valid paths
        else:
            if node.children[x_bit] is not None:
                node = node.children[x_bit]
            else:
                return count  # No more valid paths
  
    return count

3. Edge Case: Empty Array or Single Element

Pitfall: While the current code handles these cases correctly (returns 0), developers might overlook testing these scenarios or might try to optimize by adding early returns that break the logic.

Solution: Keep the implementation clean and let the loop naturally handle edge cases:

# The current implementation correctly handles:
# - Empty array: loop doesn't execute, returns 0
# - Single element: first iteration finds 0 pairs, inserts, returns 0
# No special casing needed!

4. Integer Overflow in Other Languages

Pitfall: When porting this solution to languages with fixed-size integers (like C++ or Java), bit operations might behave differently for negative numbers or cause overflow.

Solution for Other Languages:

# In Python, this isn't an issue, but if porting:
# 1. Use unsigned integers where possible
# 2. Explicitly mask results: (x >> i) & 1
# 3. Be careful with the XOR operation on signed integers
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More