最大异或对问题
最大异或对问题:从暴力到Trie树的高效解法
二、异或运算的核心性质
异或(XOR,记为 ⊕\oplus⊕)是一种位运算,其规则为:
- 两个二进制位相同时,结果为000(0⊕0=00 \oplus 0 = 00⊕0=0,1⊕1=01 \oplus 1 = 01⊕1=0)
- 两个二进制位不同时,结果为111(0⊕1=10 \oplus 1 = 10⊕1=1,1⊕0=11 \oplus 0 = 11⊕0=1)
关键观察:异或结果的大小由二进制高位决定。例如,若两个数的第 kkk 位(从000开始计数)异或为111,而其他数的第 kkk 位异或为000,则前者结果一定更大。因此,最大化异或值的核心是:让更高位的二进制位尽可能异或为111。
三、暴力解法:思路与局限
思路
枚举所有可能的数对 (Ai,Aj)(A_i, A_j)(Ai,Aj)(i<ji < ji<j),计算 Ai⊕AjA_i \oplus A_jAi⊕Aj 并记录最大值。
代码示意
int maxXor = 0;
for (int i = 0; i < n; i++) {for (int j = i + 1; j < n; j++) {maxXor = max(maxXor, a[i] ^ a[j]);}
}
复杂度分析
- 时间复杂度:O(N2)O(N^2)O(N2)。当 N=105N = 10^5N=105 时,运算次数约为 101010^{10}1010,远超计算机每秒约 10810^8108 次运算的能力,必然超时。
- 空间复杂度:O(1)O(1)O(1)。
结论:暴力解法仅适用于 N≤1000N \leq 1000N≤1000 的小规模数据,对本题不适用。
四、优化思路:Trie树的妙用
为了降低时间复杂度,我们需要一种能快速找到“与当前数异或最大的数”的方法。Trie树(字典树)正是解决这一问题的理想工具。
核心思想
- 二进制高位优先存储:将每个数的二进制表示(从最高位到最低位)存入Trie树,利用Trie的前缀共享特性高效存储。
- 贪心查询:对每个数 AiA_iAi,在Trie树中寻找能使其异或结果最大的数——即每一位尽可能选择与 AiA_iAi 对应位相反的二进制位。
五、Trie树的设计与实现
1. Trie节点结构
Trie树的每个节点仅需存储两个子节点,分别对应二进制位000和111:
struct TrieNode {TrieNode* children[2]; // 0和1两个子节点TrieNode() {children[0] = children[1] = nullptr; // 初始化子节点为空}
};
2. 插入操作:将数存入Trie树
将一个数的二进制表示从最高位到最低位插入Trie树(因 Ai<231A_i < 2^{31}Ai<231,最高位为第303030位):
- 提取第 iii 位的二进制值:(num>>i)&1(num >> i) \& 1(num>>i)&1
- 若对应子节点不存在,则创建新节点
- 沿当前位对应的子节点继续深入
void insert(TrieNode* root, int num) {TrieNode* node = root;for (int i = 30; i >= 0; --i) { // 从最高位(30)到最低位(0)int bit = (num >> i) & 1; // 提取第i位if (!node->children[bit]) {node->children[bit] = new TrieNode(); // 创建新节点}node = node->children[bit]; // 移动到子节点}
}
3. 查询操作:寻找最大异或值
对给定数 numnumnum,在Trie树中寻找能使其异或结果最大的数:
- 对每一位,优先选择与 numnumnum 当前位相反的子节点(使该位异或为111)
- 若相反位的子节点不存在,则选择相同位的子节点
- 累加每一位的贡献(1<<i1 << i1<<i 表示第iii位为111时的数值)
int query(TrieNode* root, int num) {TrieNode* node = root;int max_xor = 0;for (int i = 30; i >= 0; --i) {int bit = (num >> i) & 1;int desired_bit = 1 - bit; // 希望找到相反的位if (node->children[desired_bit]) {max_xor |= (1 << i); // 该位异或为1,计入结果node = node->children[desired_bit];} else {node = node->children[bit]; // 只能选择相同位}}return max_xor;
}
4. 整体流程
- 初始化Trie树,插入第一个数
- 对剩余每个数:
- 查询当前Trie树中能与它形成的最大异或值
- 更新全局最大值
- 将该数插入Trie树(供后续数查询)
int main() {int n;cin >> n;vector<int> a(n);for (int i = 0; i < n; ++i) {cin >> a[i];}TrieNode* root = new TrieNode();insert(root, a[0]); // 插入第一个数int max_val = 0;for (int i = 1; i < n; ++i) {max_val = max(max_val, query(root, a[i])); // 查询并更新最大值insert(root, a[i]); // 插入当前数}cout << max_val << endl;return 0;
}
六、复杂度分析
-
时间复杂度:
插入和查询每个数都需要处理313131个二进制位(000~303030),因此总时间复杂度为 O(N×31)O(N \times 31)O(N×31),即 O(N)O(N)O(N)(313131为常数)。对于 N=105N = 10^5N=105,总操作约为 3.1×1063.1 \times 10^63.1×106,完全满足时间要求。 -
空间复杂度:
最坏情况下,每个数的二进制位都不共享前缀,空间复杂度为 O(N×31)O(N \times 31)O(N×31),即 O(N)O(N)O(N),可接受。
七、样例解析
以输入 3 1 2 3
为例,详细步骤如下:
-
二进制表示:
1=0b011 = 0b011=0b01(简化为222位,实际处理313131位)
2=0b102 = 0b102=0b10
3=0b113 = 0b113=0b11 -
插入第一个数111:
Trie树路径:根 → 000(第111位) → 111(第000位) -
处理第二个数222:
- 查询:222的二进制为101010,从高位开始:
第111位是111 → 优先找000(存在),贡献 1<<1=21 << 1 = 21<<1=2
第000位是000 → 优先找111(存在),贡献 1<<0=11 << 0 = 11<<0=1
总异或值:2+1=32 + 1 = 32+1=3 - 更新max_valmax\_valmax_val为333,插入222到Trie树
- 查询:222的二进制为101010,从高位开始:
-
处理第三个数333:
- 查询:333的二进制为111111,从高位开始:
第111位是111 → 找000(存在),贡献 222
第000位是111 → 找000(不存在,只能找111),无贡献
总异或值:222 - max_valmax\_valmax_val仍为333,插入333到Trie树
- 查询:333的二进制为111111,从高位开始:
-
最终输出:333
八、总结
最大异或对问题的高效解法体现了两个核心思想:
- 二进制高位优先:利用异或运算的特性,优先保证高位为111以最大化结果。
- Trie树优化:将二进制存储与查询的复杂度从 O(N)O(N)O(N) 降至 O(31)O(31)O(31),实现整体线性时间复杂度。
这种思路不仅适用于本题,还可推广到“最大异或子集”“异或前缀和”等类似问题,是信奥中处理二进制位运算的重要技巧。