LeetCode 421. 数组中两个数的最大异或值
题目描述

题意分析
从数组中选择两个数,使异或结果最大;题目允许选择同一个位置,其异或结果为
0。异或的每一位在两个数对应位不同时为1,相同时为0。数值范围是
0到2^31 - 1,只需要处理第30位到第0位。可以逐个处理数组元素,为当前数寻找此前元素中最有利的配对,再取所有查询结果的最大值。
解法:0-1 Trie + 高位贪心
核心思路
[!blue]
把已经处理过的数存入二进制字典树。每个数从第
30位到第0位走一条完整路径,节点的两个孩子分别代表下一位取0或1;多个数具有相同高位前缀时,共用同一段路径。查询当前数时也从高位开始。若当前位是
current,优先选择wanted = current ^ 1对应的孩子,让异或结果的这一位为1。在更高位已经确定的前提下,第bit位的贡献是 $2^{bit}$,而所有更低位加起来最多只有 $2^{bit}-1$,因此只要能让当前位为1,就不应为了低位而放弃它。每次选择都沿当前节点的某个现存孩子继续,相当于只保留与已选前缀一致的候选数,保证最后得到的路径属于一个真实存入的数。不能在不同候选之间独立挑选各位,否则拼出的配对可能不存在。
相反位分支存在时,把查询结果
value的对应位设为1;不存在时只能走同位分支,该位贡献0。字典树中至少有一个完整的数,因此走到的节点总有下一层可选分支,直到全部31位处理完。先插入第一个数,之后每个数都先查询、再插入。任意两个不同位置的配对,都会在处理较后位置时进入候选范围;同一位置与自身异或只会得到
0,由初始答案覆盖即可。这样也自然处理了只有一个元素的情况。
解题步骤
- 创建空字典树,从高位到低位插入
nums[0],不存在的分支就新建节点。- 将全局答案初始化为
0,从第二个数开始处理。- 查询时从根出发,在每一位优先走与当前数相反的分支,并设置异或结果对应位;没有相反分支时走同位分支。
- 查询结束后,用这次得到的最大异或值更新全局答案,再把当前数插入字典树。
- 所有数处理完后返回答案。
代码实现
class Solution {
private static class Node {
Node[] children = new Node[2];
}
public int findMaximumXOR(int[] nums) {
Node root = new Node();
insert(root, nums[0]);
int answer = 0;
for (int i = 1; i < nums.length; i++) {
// 先查询此前的实际候选,再插入当前数
answer = Math.max(answer, query(root, nums[i]));
insert(root, nums[i]);
}
return answer;
}
private void insert(Node root, int num) {
Node node = root;
for (int bit = 30; bit >= 0; bit--) {
int value = (num >>> bit) & 1;
if (node.children[value] == null) {
node.children[value] = new Node();
}
node = node.children[value];
}
}
private int query(Node root, int num) {
Node node = root;
int value = 0;
for (int bit = 30; bit >= 0; bit--) {
int current = (num >>> bit) & 1;
// 优先让更高位为一,之后仍沿同一候选分支继续
int wanted = current ^ 1;
if (node.children[wanted] != null) {
value |= 1 << bit;
node = node.children[wanted];
} else {
node = node.children[current];
}
}
return value;
}
}
type xorNode struct {
children [2]*xorNode
}
func findMaximumXOR(nums []int) int {
root := &xorNode{}
insertXOR(root, nums[0])
answer := 0
for i := 1; i < len(nums); i++ {
// 先查询此前的实际候选,再插入当前数
value := queryXOR(root, nums[i])
if value > answer {
answer = value
}
insertXOR(root, nums[i])
}
return answer
}
func insertXOR(root *xorNode, num int) {
node := root
for bit := 30; bit >= 0; bit-- {
value := (num >> bit) & 1
if node.children[value] == nil {
node.children[value] = &xorNode{}
}
node = node.children[value]
}
}
func queryXOR(root *xorNode, num int) int {
node := root
value := 0
for bit := 30; bit >= 0; bit-- {
current := (num >> bit) & 1
// 优先让更高位为一,之后仍沿同一候选分支继续
wanted := current ^ 1
if node.children[wanted] != nil {
value |= 1 << bit
node = node.children[wanted]
} else {
node = node.children[current]
}
}
return value
}
复杂度分析
- 时间复杂度:$O(31n)$,每个数的插入和查询都只经过
31层;位宽固定时即为 $O(n)$。- 空间复杂度:$O(31n)$,每次插入最多新增
31个节点,实际节点数会因共享前缀而减少。
关键点总结
[!green]
- 贪心依据是高位的权重大于所有低位之和,而不是只尽量增加异或结果中
1的个数。- 字典树路径保留候选数之间的位关联,保证每次查询对应一个真实配对。
- 查询得到的是最大异或值,不需要额外还原配对元素本身。
- 先查询再插入逐步加入候选;重复数可以共用同一条路径。
易错点总结
[!yellow]
- 必须从高位向低位贪心。先优化低位可能阻碍更高位取得
1,从而得到更小结果。- 插入与查询要使用相同的位宽和方向;本题非负
31位整数从第30位开始即可。- 相反分支不存在时不能结束查询,仍需沿同位分支继续优化后面的位。
- 查询前字典树必须非空;代码先插入第一个数,单元素输入直接返回初始答案
0。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 1707. 与数组中元素的最大异或值 | 困难 | 同样用二进制Trie贪心选择相反位,原题还限制参与查询的数值上界。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!