1803. Count Pairs With XOR in a Range
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 beforej
in the array)- The XOR operation between
nums[i]
andnums[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.
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 a0
at this position (making the XOR result definitely smaller), and then continue exploring the path that gives us a1
. - If the limit's current bit is
0
, we must follow the path that keeps our XOR result's bit as0
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]
andchildren[1]
: pointers to left (bit 0) and right (bit 1) child nodescnt
: counter tracking how many numbers pass through or end at this node
Insert Function
The insert(x)
function adds a number to the Trie:
- Start from the root node
- Process bits from most significant (bit 15) to least significant (bit 0)
- 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
- Extract the bit value:
Search Function
The search(x, limit)
function counts how many numbers in the Trie have XOR with x
less than limit
:
- Start from the root with
ans = 0
- 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
- 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]
- We must follow the path where XOR bit = 0 to stay under limit
- Move to:
node = node.children[v]
(same bit asx
)
- Get current bit of
Main Algorithm
The countPairs
function orchestrates the solution:
- Initialize answer counter and empty Trie
- For each number
x
innums
:- 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
- Calculate pairs with XOR in
- 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 processingnums[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 EvaluatorExample 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)
andtree.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), takingO(log M)
time wherelog M = 16
. - It performs one
insert
operation:tree.insert(x)
. The insert operation also traverses 16 bits to insert the number into the Trie, takingO(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 isO(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
Which of the following array represent a max heap?
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!