Xor Sum HDU - 4825(01字典树)

本文介绍了一种使用01字典树解决异或最大值问题的方法。通过构建特殊的01字典树,文章详细解释了如何高效地找到与给定数异或结果最大的数。该方法适用于数组中寻找连续子段异或和的最大值问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Xor Sum

HDU - 4825

Zeus 和 Prometheus 做了一个游戏,Prometheus 给 Zeus 一个集合,集合中包含了N个正整数,随后 Prometheus 将向 Zeus 发起M次询问,每次询问中包含一个正整数 S ,之后 Zeus 需要在集合当中找出一个正整数 K ,使得 K 与 S 的异或结果最大。Prometheus 为了让 Zeus 看到人类的伟大,随即同意 Zeus 可以向人类求助。你能证明人类的智慧么?

Input 输入包含若干组测试数据,每组测试数据包含若干行。
输入的第一行是一个整数T(T < 10),表示共有T组数据。
每组数据的第一行输入两个正整数N,M(<1=N,M<=100000),接下来一行,包含N个正整数,代表 Zeus 的获得的集合,之后M行,每行一个正整数S,代表 Prometheus 询问的正整数。所有正整数均不超过2^32。 Output 对于每组数据,首先需要输出单独一行”Case #?:”,其中问号处应填入当前的数据组数,组数从1开始计算。
对于每个询问,输出一个正整数K,使得K与S异或值最大。 Sample Input
2
3 2
3 4 5
1
5
4 1
4 6 5 6
3
Sample Output
Case #1:
4
3
Case #2:
4

01字典树模板:

01字典树问题,它是用来解决xor的有力武器,通常是给你一个数组,问你一段连续的异或和最大是多少,正常思路贪心dp啥的都会一头雾水,但是用01字典树就能很快的解决,实现起来也十分方便。

将要插入的数的二进制位倒着建树(为什么?因为异或时高位尽量大,结果才尽量大),即高位在深度低的节点上

贴一个01字典树的普遍模版

  1. #define Memset(x, a) memset(x, a, sizeof(x))  
  2. typedef long long ll;  
  3. const int maxn = 100000 + 5;//集合中的数字个数  
  4. int ch[32*maxn][2];         //节点的边信息  
  5. ll val[32*maxn];            //节点存储的值  
  6. int sz;                     //树中当前节点个数  
  7.   
  8. void init(){  
  9.     Memset(ch[0],0);           //树清空  
  10.     sz=1;  
  11. }  
  12.   
  13. void _insert(ll a){//在字典树中插入 a  
  14.                   //和一般字典树的操作相同 将X的二进制插入到字典树中  
  15.     int u=0;  
  16.     for(int i=32;i>=0;i--){  
  17.         int c=((a>>i)&1);  
  18.         if(!ch[u][c]){  
  19.             Memset(ch[sz],0);  
  20.             val[sz]=0;  
  21.             ch[u][c]=sz++;  
  22.         }  
  23.         u=ch[u][c];  
  24.     }  
  25.     val[u]=a;     //最后的节点插入value  
  26. }  
  27.   
  28. ll query(ll a){   //在字典树中查找和a异或的值最大的元素b 返回b的值  
  29.     int u=0;  
  30.     for(int i=32;i>=0;i--){  
  31.         int c=((a>>i)&1);  
  32.         if(ch[u][c^1]) u=ch[u][c^1];//c=0,b=c^1=1,b^c=1;c=1,b=c^1=0,b^c=1;  
  33.         else u=ch[u][c];  
  34.     }  
  35.     return val[u];  


ac code:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long ll;
const int maxn = 1e5+5;
int tree[32*maxn][2];
ll val[32*maxn];//节点储存的值
int sz;//树中当前节点个数
void init(){
    memset(tree[0],0,sizeof(tree[0]));
    sz = 1;
}
void Insert(ll a){
    int u = 0;
    for(int i = 32; i >= 0; i--){
        int c = ((a>>i)&1);//确定最高位的二进制码
        if(!tree[u][c]){//如果这个位置是个空,则将这个位置标上节点号,并创建它的子节点
            memset(tree[sz],0,sizeof(tree[sz]));
            val[sz] = 0;
            tree[u][c] = sz++;//存下这个位置的节点号
        }
        u = tree[u][c];//转到下一节点
    }
    val[u] = a;//最后存完后在最后节点存上这个数值
}
ll query(ll a){
    int u = 0;
    for(int i = 32; i >= 0; i--){
        int c = ((a>>i)&1);
        if(tree[u][c^1]) u = tree[u][c^1];//c = 0,c^1 = 1;c = 1,c^1 = 0;尽可能在高位找使它异或为1的
        else u = tree[u][c];
    }
    return val[u];
}
int main(){
    int t,n,m;
    scanf("%d",&t);
    for(int cas = 1; cas <= t; cas++){
        init();
        scanf("%d%d",&n,&m);
        ll a;
        for(int i = 0; i < n; i++){
            scanf("%lld",&a);
            Insert(a);
        }
        printf("Case #%d:\n",cas);
        for(int i = 0; i < m; i++){
            scanf("%lld",&a);
            printf("%lld\n",query(a));
        }
    }
    return 0;
}


<think>嗯,用户让我解析这段Java代码,特别是int lsb = ...这一行。首先,我得回忆一下这个算法的整体思路。题目是找出数组中只出现一次的两个数字,其他数字都出现两次。这种情况下,通常会用异或操作,因为相同的数字异或会抵消,结果为0。但如果有两个不同的单独数字,异或的结果会是这两个数的异或值,所以需要进一步处理。 那代码的大致流程应该是这样的:首先,对所有数进行异或,得到xorsum,这个xorsum实际上就是那两个单独数字的异或结果。因为其他数字成对出现,异或后为0。接下来的问题是如何把这两个数字分开。这时候需要找到xorsum中的一个二进制位,这个位在两个单独数字中一个是1,另一个是0。这样就能将原数组分成两组,每组各包含一个单独数字,然后分别异或处理。 现在关键点在于如何找到这样的一个二进制位。代码中用了xorsum & (-xorsum)来获取最低有效位(LSB),也就是最右边的1所在的位。但这里有个特殊情况判断,如果xorsum是Integer.MIN_VALUE,即二进制1000...000,这时候xorsum本身就是负数,取负数的话可能会溢出?或者是因为在这种情况下,xorsum & (-xorsum)的结果是否正确? 比如,当xorsum是Integer.MIN_VALUE时,-xorsum在Java中会溢出,因为int的范围是-2^31到2^31-1,所以Integer.MIN_VALUE的绝对值超过了最大正数,所以-xorsum的结果还是Integer.MIN_VALUE?这时候xorsum & (-xorsum)就等于xorsum本身,所以这种情况下直接取xorsum。否则的话,用xorsum & (-xorsum)来得到LSB。 这一步的意义是为了找到两个单独数字不同的最低有效位,这样可以根据这一位将数组分成两部分。然后对这两部分分别进行异或,最终得到两个单独的数字。 那用户的问题主要是这一步为什么要这么做,以及为什么要处理Integer.MIN_VALUE的情况。可能用户对位运算中的负数处理不太清楚,或者对溢出情况有疑问。需要解释清楚补码表示下的负数,以及如何用x & -x来得到最低有效位的原理。 可能还需要举例子说明。比如,假设xorsum是某个数,比如6(二进制0110),那么-xorsum在补码中是1010(假设4位的话),然后x & -x得到0010,即最低位的1。这时候就能用这个位来区分两个不同的数字。 但当xorsum是Integer.MIN_VALUE时,即二进制1后面31个0,那么-xorsum在补码中是无法表示的,因为int的范围到不了+2^31,所以这时候的-xorsum会溢出,导致结果还是Integer.MIN_VALUE本身?或者可能这时候的计算结果是否正确? 比如,在Java中,对于Integer.MIN_VALUE取负数,实际上还是Integer.MIN_VALUE,因为溢出。所以在xorsum为Integer.MIN_VALUE时,xorsum & (-xorsum)的结果就是xorsum本身,这时候作为掩码是否正确?这个时候只有最高位是1,所以根据这个掩码分组的话,其他数的该位是否为1会被分组。这时候是否正确? 比如,假设两个单独的数字是x和y,它们的异或结果是x ^ y = Integer.MIN_VALUE,即最高位不同。这时候用这个掩码的话,x和y在这一位一个是1,另一个是0。所以这时候的分组是正确的,可以正确地将两个数分开。因此,这一步的处理是正确的,不需要特殊处理?或者可能当xorsum是Integer.MIN_VALUE时,xorsum & (-xorsum)就等于xorsum本身,所以这时候直接使用即可,但为什么代码中要单独处理这个情况? 可能是在这种情况下,xorsum & (-xorsum)的结果会因为溢出而不正确?或者是否在Java中,当xorsum是Integer.MIN_VALUE时,-xorsum仍然是Integer.MIN_VALUE(因为溢出),所以这时候进行&运算的结果就是xorsum本身。这时候,掩码是正确的,因为最低有效位就是最高位。所以这个条件判断可能只是为了防止溢出带来的问题? 或者可能当xorsum是Integer.MIN_VALUE的时候,xorsum & (-xorsum)的计算会得到正确的掩码,所以不需要特殊处理,但代码中作者可能为了保险起见,或者在某些情况下可能出错,所以直接使用xorsum? 比如,假设xorsum是Integer.MIN_VALUE,那么它的二进制是10000000 00000000 00000000 00000000。在Java中,-xorsum的计算是取反加1,即补码运算。原数的补码是它本身(因为是负数)。取反的话,得到01111111 ... 1111,加1得到10000000 ... 0000,所以-xorsum等于xorsum本身。这时候xorsum & (-xorsum)的结果就是xorsum。这时候,lsb的值就是xorsum本身,也就是最高位的1。这时候,用这个掩码来分割数组是正确的,因为两个单独的数字在这一位上是不同的。因此,这个条件判断是否是多余的? 可能作者考虑到当xorsum是Integer.MIN_VALUE时,-xorsum会导致溢出,从而使得xorsum & (-xorsum)的结果仍然是xorsum,这在逻辑上是正确的。所以,这个时候的条件判断是否必要? 例如,假设xorsum是Integer.MIN_VALUE,那么xorsum & (-xorsum)的结果等于xorsum,而这时候xorsum & (-xorsum)的结果确实是正确的掩码(唯一的1位),所以此时条件判断是否多余? 可能作者在这里是为了处理可能的情况,即当xorsum为Integer.MIN_VALUE时,直接使用xorsum本身作为掩码,而不用做位运算,因为在这种情况下,-xorsum会导致溢出,但位运算的结果仍然正确。或者,是否在某些情况下,当xorsum是Integer.MIN_VALUE时,xorsum & (-xorsum)的行为会有问题? 或者,可能作者在这里的考虑是,当xorsum是Integer.MIN_VALUE时,xorsum & (-xorsum)的结果还是xorsum,而其他情况下,用xorsum & (-xorsum)来得到最低有效位。所以条件判断可能只是为了避免潜在的溢出问题,但实际上在这种情况下,位运算的结果是正确的,所以条件判断可能并非必须,但作者出于谨慎添加了。 所以,这一步的目的是找到xorsum中最右侧的1的位置,作为分组的依据。例如,如果xorsum是6(二进制0110),那么xorsum & (-xorsum)得到0010,即第二个bit位。这样,数组中所有数的这个bit位为1的分为一组,剩下的为另一组。这样就能确保两个单独的数被分到不同的组中,而其他相同的数会被分到同一组,从而异或后消去。 总结来说,lsb这一步是为了找到两个单独数不同的最低有效位,从而将数组分成两组,每组各包含一个单独数,然后通过异或找出这两个数。而处理Integer.MIN_VALUE的情况可能是为了避免溢出或者确保正确性。</think>我们来逐步解析这段代码,特别是关键步骤 `int lsb = ...` 的作用。 --- ### **代码整体思路** 题目要求:**在数组中找出仅出现一次的两个数字,其余数字均出现两次。** 代码核心思路: 1. **异或性质**:相同数字异或结果为 $0$,不同数字异或结果为 $1$。因此,对所有数字异或后,最终结果 `xorsum` 等于两个目标数字的异或值。 2. **分组**:通过 `xorsum` 的某一位将数组分为两组,每组包含一个目标数字,其他数字成对出现,最终通过异或消去。 --- ### **关键代码解析** #### **1. 计算异或总和** ```java int xorsum = 0; for (int num : nums) { xorsum ^= num; ``` - **目的**:得到两个目标数字的异或值 `xorsum`。例如,若目标数字是 $a$ 和 $b$,则 `xorsum = a ^ b`。 --- #### **2. 确定最低有效位(LSB)** ```java int lsb = (xorsum == Integer.MIN_VALUE ? xorsum : xorsum & (-xorsum)); ``` - **作用**:找到 `xorsum` 中最右侧的 $1$ 的位置(即最低有效位),用于分组。 - **原理**: - `x & (-x)` 在二进制中会保留最右侧的 $1$,其余位设为 $0$。例如: - 若 $x = 6$(二进制 $0110$),则 $-x = 1010$(补码表示),$x \& (-x) = 0010$。 - **特殊情况**:当 `xorsum` 是 `Integer.MIN_VALUE`(二进制 $1000\cdots0000$),其补码表示为自身,直接取 `xorsum` 本身作为掩码。 --- #### **3. 分组异或** ```java int type1 = 0, type2 = 0; for (int num : nums) { if ((num & lsb) != 0) { type1 ^= num; } else { type2 ^= num; } } ``` - **步骤**: 1. 根据 `lsb` 将数组分为两组: - 第 $1$ 组:数字在 `lsb` 对应位为 $1$。 - 第 $2$ 组:数字在 `lsb` 对应位为 $0$。 2. 每组分别异或,结果即为两个目标数字。 --- ### **示例说明** 假设数组为 $[1, 2, 1, 3, 2, 5]$,目标数字是 $3$ 和 $5$: 1. **计算 `xorsum`**: $3$ ^ $5 = 6$(二进制 $0110$)。 2. **确定 `lsb`**: $6 \& (-6) = 0010$(二进制)。 3. **分组**: - 第 $1$ 组(`lsb` 位为 $1$):$[2, 3, 2]$ → 异或结果为 $3$。 - 第 $2$ 组(`lsb` 位为 $0$):$[1, 1, 5]$ → 异或结果为 $5$。 --- ### **总结** - **`lsb` 的作用**:通过异或结果的某一位差异,将数组分为两组,确保两个目标数字被分离。 - **特殊处理 `Integer.MIN_VALUE`**:防止补码运算溢出导致掩码错误,直接使用其本身作为掩码。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值