CF842D 正解解析

CF842D 正解解析

题意

问题描述

mex 是一个序列中没有出现过的最小非负整数。

给出你一个长度为 \(n\) 的非负整数序列以及 \(m\) 个询问,每次询问先给你一个整数 \(x\),然后:

  • 把序列中所有数异或上 \(x\)
  • 输出序列的 mex

注意,在每个询问过后序列是发生变化的。

输入格式

第一行 \(n\)\(m\) \((1 \leq n, m, \leq 3 \times 10^5)\)

下一行 \(n\) 个整数 \(a_i\) \((0 \leq a_i \leq 3 \times 10^5)\)

\(m\) 行每行一个整数 \(x\) \((0 \leq x \leq 3 \times 10^5)\)

输出格式

对于每个询问输出你的答案并换行。

解题

我的理解

使用 \(\rm 01 Trie\) 解决,将 \(n\) 个数正常插入。

当考虑异或 \(x\) 时,从根节点向下查找,如果 \(x\) 的第 \(i\)\(2\) 进制位为 \(1\),说明这个子树中所有 \(a_i\) 的第 \(i\) 个二进制位会进行反转(\(0 \oplus 0 = 0, 0 \oplus 1 = 1\) 不变;\(1 \oplus 0 = 1, 1 \oplus 1 = 0\) 反转),我们向 \(1\) 的边走,就可以做出反转的效果。

向下查找时,尽可能走与 \(x\) 的二进制位相同的边,那么异或出的结果就会尽可能小。但是,mex 要求序列中没有出现过,所以如果一个节点下方的子树(包括这个节点)已经被 \(a_i\) 放满,那么不管怎么反转,都没有找出未出现的数的可能,则应走另一条边,并统计上这一层的贡献。

\(u\) 节点变为 \(0\) 时,说明不存在这一分支,找到了最小的未出现值,即 mex,可以返回答案。

\(Code\)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

const int NR = 3e5 + 10;
int a[NR];
int sz[NR * 20];
int trie[NR * 20][2];
int cnt = 1;

void insert(int u, int x, int now)
{
	if (now == -1)
	{
		sz[u] = 1;
		return;
	}
	int num = (x >> now) & 1;
	if (trie[u][num] == 0) trie[u][num] = ++cnt;
	insert(trie[u][num], x, now - 1);
	sz[u] = sz[trie[u][0]] + sz[trie[u][1]];
}

int query(int x)
{
	int res = 0;
	int u = 1;
	for (int i = 18; i >= 0; i --)
	{
		int num = (x >> i) & 1;
		if (sz[trie[u][num]] == (1 << i))
		{
			u = trie[u][num ^ 1];
			res |= (1 << i);
		}
		else
		{
			u = trie[u][num];
		}
		if (u == 0) return res;
	}
	return res;
}

int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++)
	{
		scanf("%d", &a[i]);
		insert(1, a[i], 18);
	}
	int nx = 0;
	while (m --)
	{
		int x;
		scanf("%d", &x);
		nx ^= x;
		printf("%d\n", query(nx));
	}
	return 0;
}

一篇题解和豆包的讲解

题目分析

感觉这应该是个经典问题,我之前却不知道。实际上题目就是询问集合中的数都异或上某个数后的 \(mex\) 值。

思路探索

考虑常用的操作异或集合的手段,首先想到线性基。然而线性基只能插入,不能修改,而且也不能查询 \(mex\) 这么奇怪的东西。

先不考虑异或,显然\(mex\)是有可二分性的。我们需要一个数据结构支持查询小于某个数的数是否都出现过,然后就可以二分了。第一反应是值域线段树,它显然是可以做的,直接在线段树上二分就行。具体来说,走到线段树的某个节点时,看一下左儿子存在的数是不是等于左儿子对应的值域大小,即判断左儿子是不是满的。但是,线段树也不能维护异或操作。

实际上我们可以选取和线段树结构相同的另一个数据结构(或者说它们就是同一个东西)—— \(01trie\)。我们也可以在 \(01trie\) 上二分查 \(mex\),并且走到 \(trie\) 上某个点时,如果异或的数这一位为 \(1\),意义就是交换 \(trie\) 的左右儿子,然后就可以用和线段树一样的方法做了。

有一个细节就是,在计算 \(trie\) 一个点的 \(size\) 时,注意一个数出现多次只计算一次。

其他具体实现可以看一下代码,还是比较易懂的。

代码实现

#include <cstdio>
#include <iostream>
using namespace std;
const int maxn=3e5+5,Lim=18;
int tr[maxn*19][2],nowxor,cnt=1,siz[maxn*19],n,m;
template<typename T>
inline void read(T &x)
{
    char c;int f=1;
    while(!isdigit(c=getchar())) (c=='-')&&(f=-1);
    x=c^48;
    while(isdigit(c=getchar())) x=x*10+(c^48);
    x*=f;
}
void insert(int x,int u=1,int now=Lim)
{
    if(now==-1) return siz[u]=1,void();
    int s=x>>now&1;
    if(!tr[u][s]) tr[u][s]=++cnt;
    insert(x,tr[u][s],now-1);
    siz[u]=siz[tr[u][0]]+siz[tr[u][1]];
}
inline int query(int x=nowxor)
{
    int u=1,res=0;
    for(int i=Lim;i>=0;--i)
    {
        int s=x>>i&1;
        if(siz[tr[u][s]]==((1<<i))) u=tr[u][s^1],res|=(1<<i);
        else u=tr[u][s];
        if(!u) return res;
    }
    return res;
}
int main()
{
    int x;
    read(n);read(m);
    for(int i=1;i<=n;++i)
    {
        read(x),insert(x);
    }
    while(m--)
    {
        read(x);nowxor^=x;
        printf("%d\n",query());
    }
    return 0;
}

Qs ans As by doubao

Q1

解读 query 函数的正确性

A1

query 函数用于在异或集合中查询 mex 值,其正确性基于以下几点:

  1. 二分查找思路mex 具有可二分性,即如果小于某个数 x 的所有数都在集合中出现过,而 x 不在集合中,那么 mex 值就是 x。函数通过从最高位到最低位遍历二进制位来二分查找 mex 值。从 Lim(代码中定义为 \(18\))开始,每次循环检查当前位 i ,这样可以逐步确定 mex 值的每一位。
  2. 基于 01-trie 的查询:利用 01-trie 数据结构,它和线段树结构相似,但能更好地维护异或操作。在 01-trie 中,从根节点开始向下遍历,根据当前查询数 x 的二进制位决定走哪条分支。如果异或的数这一位为 \(1\),就交换 trie 的左右儿子,模拟了在线段树上二分查找时检查左子树是否满的操作。
  3. 判断条件:在遍历 01-trie 的过程中,通过比较 trie 节点的 siz(表示以该节点为根的子树中不同数的个数)和当前位对应的值域大小(1<<i)来决定下一步的走向。如果当前节点对应分支的 siz 等于当前位对应的值域大小,说明该分支的所有可能值都存在,此时需要换另一个分支继续查找,并将当前位加入结果 res;否则,直接沿着当前分支继续查找。
  4. 边界情况处理:如果在遍历过程中遇到节点 u\(0\)(即不存在对应分支),说明已经找到 mex 值,直接返回 res。遍历完所有位后,返回最终的 res,即为集合异或某个数后的 mex 值。

综上,query 函数通过合理利用二分查找和 01-trie 数据结构,正确地实现了在异或集合中查询 mex 值的功能。

Q2

u=0说明什么

A2

query 函数的执行过程中,u = 0 说明在 01-trie 上进行二分查找 mex 值的过程中,当前查找路径下已经不存在满足条件的数,即已经找到了集合异或某个数后的 mex 值。

  • 01-trie 是一种用于高效处理二进制数据的数据结构,在本题中用于查询集合异或某个数后的 mex 值。在 query 函数中,从根节点(u = 1)开始,根据异或值 x 的二进制位逐位向下遍历 01-trie。
  • u = 0 时,意味着在当前遍历到的这一位,不存在对应的子节点。这表明在当前查找路径下,已经无法找到比当前结果 res 更小且未在集合中出现的数,此时的 res 就是集合异或 x 后的 mex 值,所以直接返回 res 。如代码中所示,if (!u) return res; ,当 u 为 0 时,直接返回当前的 res 作为查询结果。

老师讲解

  1. 多次异或的结果保留:\(a_i \oplus x_1 \oplus x_2 = ai \oplus (x_1 \oplus x_2)\) 相当于每次只对 \(a\) 数组但次异或。
  2. \(a_i\) 异或 \(x\) 中最小不存在的数 \(=\) \(a_i\) 中最小不存在的数异或 \(x\)
posted @ 2025-02-06 14:19  hsy8116  阅读(8)  评论(0)    收藏  举报