AcWing 143. 最大异或对 —— 神奇的二进制

2023.01.11 补数组大小分析:

在构建 tire 树中设 son 数组大小为 [ M ] [ 2 ] [M][2] [M][2],其中 M = N ∗ 31 M=N * 31 M=N31,可是为什么是这样呢?下面以 N N N 4 4 4 来模拟一下题目,并给出解释。
N N N 4 4 4 即有 4 4 4 个输入如下所示:
a [ 0 ] a[0] a[0]:0000 0000 0000 0000 0000 0000 0000 000
a [ 1 ] a[1] a[1]:0100 0000 0000 0000 0000 0000 0000 000
a [ 2 ] a[2] a[2]:1000 0000 0000 0000 0000 0000 0000 000
a [ 3 ] a[3] a[3]:1100 0000 0000 0000 0000 0000 0000 000

有不少同学觉得 M M M 的大小应该是 N N N 31 31 31。这是远远不够的,注意看,此时输入的 N N N 个数都不尽相同。
观察下图的 tire 树(为了看起来容易些,将所有的树叉都花了出来,单实际上在第 k k k 个数输入前只有 k − 1 k - 1 k1 根树叉)和 son 数组,由于存储的实际上是 i d x idx idx因此有多少个在不同位置上不同的二进制数就有多少个不同的 i d x idx idx,本例比较极端,几乎用到了 4 ∗ 31 = 124 4 * 31 = 124 431=124 全部的位置,但实际上题目不会这么极端,因此设置为 N ∗ 31 N * 31 N31 是足够的。

AcWing 143. 最大异或对 —— 神奇的二进制_第1张图片


题目描述


分析:

这里先回顾一下异或的操作。异或(Exclusive or, XOR)为当两两数值相同时为否,而数值不同时为真。在编程语言中,常表示为 p ^ q。直观如下:

AcWing 143. 最大异或对 —— 神奇的二进制_第2张图片

在本题当中可以对给出的 N N N 个整数进行暴力枚举同时记录两两最大的异或结果。很不幸暴力的做法会超时,下面给出暴力代码:

// 起码是 0(两个一样的数)
int res = 0
for (int i = 0; i < n; i ++) // 枚举第一个数
{
	for (int j = 0; j < i; j ++) // 枚举第二个数,可以避免重复枚举
		res = max(res, a[i] ^ a[j])
}

那我们要进行优化,具体为对内层循环进行优化。由于数据小于 2 31 2^{31} 231,因此可以将每个整数看成长度为 31 31 31 位的二进制串。那能使 a [ i ] a[i] a[i] 与其异或值最大的整数一定是高位至低位与 a [ i ] a[i] a[i]有尽可能多的不同位(使异或结果可以尽可能多地获得1)。流程如下所示,假设由 a [ 0 ] a[0] a[0] a [ i − 1 ] a[i-1] a[i1] 已经构建好了trie树,且 a [ i ] a[i] a[i] 的二进制表示为 101110...1 101110...1 101110...1。在贪心准则下每次往能获得最大异或值的方向走,若其他整数不能使当前位获得最大值(假设 a [ i ] a[i] a[i] 当前位为0,而tire树中只有0可以走),也只能先将就一下往这里走,等下一位再追求最好的。直至走完 a [ i ] a[i] a[i]

AcWing 143. 最大异或对 —— 神奇的二进制_第3张图片

:对于 a [ i ] a[i] a[i] 来讲,它面临的 trie树,是由 a [ 0 ] . . . a [ i − 1 ] a[0] ... a[i-1] a[0]...a[i1] 构建的。
以上就是利用trie树优化内层的循环的思想,并且每次 a [ i ] a[i] a[i] 只和 a [ j ] a[j] a[j] ( j < i ) (j < i) (j<i) 的数进行运算即可,这是因为 p ^ q = q ^ p(^ 为异或运算)。
另外需要注意一个细节,对于每一个 a [ i ] a[i] a[i] ( 0 ≤ i ≤ N ) (0≤i≤N) (0iN)是先进行对其进行最大异或值查询再插入进 trie树还是先插入再查询呢?这里我们采用先插入再查询的流程,因为对于第一个整数来讲,trie树为空,若此时进行异或运算可能会非法的结果;若先插入再查询,则第一个数一定是和自己做异或运算,由于异或运算的性质,可以得出结果一定为 0 0 0


别扭的位运算

由于本题都是对二进制数进行操作,因此我打算先提前把位运算说清楚,这样在看代码时会事半功倍。
左移运算(<<):在C++中,若对整数进行左移操作,如 x << 1,即把每一位向前(左)推一位后在末尾补上 0 0 0,相当于将这个数放大 2 2 2倍。这是为什么呢?请看下图:

AcWing 143. 最大异或对 —— 神奇的二进制_第4张图片

右移运算(>>):在C++中,若对整数进行左移操作,如 x >> 1,即把每一位向后(右)推一位,并把推出去的数去掉,相当于将这个数缩小 2 2 2倍。具体细节可将左移推导中的乘法转换为除法即可。同理也要搞清楚左移 k k k 位以及右移 k k k 位的含义。

有了左右移的概念之后,我们来看一下代码中经常出现的位运算操作之一:x >> k & 1。大家都将该操作称为“看看第 k k k 位( k k k 0 0 0开始)”。该操作为将 x x x 的二进制表示右移 k k k 位后与 0000...01 0000...01 0000...01按位求运算。我想与运算大家肯定明白,那为什么是看看第 k k k 位呢?由于二进制只有 0 0 0 1 1 1,求与运算后若为 0 0 0,说明第 k k k 位是 0 0 0;若是 1 1 1,说明第 k k k 位是 1 1 1。例见下图:

AcWing 143. 最大异或对 —— 神奇的二进制_第5张图片


代码(C++)

#include 

using namespace std;

const int N = 100010, M = 31 * N;
int a[N], son[M][2], n, idx;

void insert(int x)
{
    int p = 0;
    for (int i = 30; i >= 0; i --)
    {
        // 取出 x 的第 i 位二进制数
        int u = x >> i & 1;
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
}

int query(int x)
{
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i --)
    {
        // 从高位向低位操作
        int u = x >> i & 1;
        // 对于当前位的二进制数
        // 尽可能往其出现过的相反的方向走,假设当前位 u = 0
        // 若1的那个方向的 trie树枝干被创建则向那个方向走
        // 若没有被创建,则先将就一下走0的方向
       if (son[p][!u])
        {
            p = son[p][!u];
            // 相当于 + 000..000u
            res = (res << 1) + !u; // 将当前位加到左移后空出的第0位上
        }
        else 
        {
            p = son[p][u];
            res = (res << 1) + u;
        }
    }
    return res;
}

int main()
{
    cin >> n;
    
    for (int i = 0; i < n; i ++) cin >> a[i];
    int res = 0;
    
    for (int i = 0; i < n; i ++)
    {
        // 先插入后运算是为了避免边界问题,对于第一个整数整个树为空
        // 若先将自己插入进去,则与自己的异或结果始终为0
        insert(a[i]);
        
        // t 为 a[0]至a[i-1] 中与a[i]异或值最大的那个整数
        // 即够成局部最大异或对
        int t = query(a[i]);
        
        // 看是否是所有整数中的最大异或对
        res = max(res, a[i] ^ t);
    }
    
    cout << res;
}


你可能感兴趣的:(AcWing,算法基础课,c++,算法,数据结构)