大致题意:n个集合,你要进行m个操作。总共有3种操作。第一种,合并两个集合x和y。第二张,把特定的集合里面所有的数字加一。第三种,询问在某个集合里面,对于所有数字对2的k次方取模后,有多少个数字等于x。
首先,合并的话实在是有太多的方式,大部分数据结构的启发式合并包括set都可以。但是主要是第三个,所有数字对2的k次方取模,然后看结果有多少个数字等于x。仔细想想这个取模,不难发现,其实就是询问,一个集合里面,在二进制下,后k位x的数字有多少个。询问与二进制和位数有关,很容易想到用字典树。
字典树的合并比较容易写,当一棵树有某个儿子,而另一个没有的时候,直接把没有的儿子指向有的,其余情况暴力合并即可。问题的关键在于这个集合整体加一怎么处理。我们考虑加一对于一个数字来说会有什么样的影响。由于我实在二进制下存储,那么我考虑影响当然也是在二进制下的。对于某一位,如果为0,那么加一之后会变成一,对其它位不产生影响;如果为1,那么加一之后会变成零,同时对下一位产生进位。因此加一操作,对于某个节点的两个儿子来说,相当于是交换了左右儿子,然后对于原本的1这个儿子,还对下一位产生了额外贡献。进一步总结我们可以发现,如果对某个点加x,这个x如果位奇数,那么和加一的情况类似;如果为偶数,那么就不会交换两个儿子,但是要对两个儿子产生x/2的进位。
如此,我们就大致有了一个处理的方法。我们可以对于每一个节点保存一个lazy标记,这个标记类似于线段树中的lazy标记。每次整个集合增加的时候,只改变lazy标记,然后在下一次访问这个节点的时候,再去把这个标记push_down。而这个push_down的方式就是按照之前说的那样,根据lazy的奇偶来判断是否应该交换儿子和额外进位。对于每一个查询操作,我们直接把放到字典树中,确定一个位置,输出对应节点的size即可。具体操作的时候还要注意,一定要把每一个插入的数字固定插入长度设置为30,因为数字的高位即使为0也是需要保存的。
最后就是复杂度分析。一次插入操作的代价是O(30)≈O(log1e9),然后对于每次的增加操作,其实即使是暴力,它的复杂度也才O(30),因为交换左右儿子之后,需要改变的只有原本为1的这个儿子,因此最多往下走深度这么多个节点。最后就是字典树的合并,考虑初始的时候,每个树中有一个数字,节点数目是O(30),然后一次合并操作是log的复杂度,合并之后会少一棵树,合并复杂度就是O(nlog1e9)。询问当然也是一个询问O(30)的复杂度。因此,最后总的时间复杂度就是O((n+q)log1e9)。具体见代码:
#include<bits/stdc++.h>
#define mod 1000000007
#define LL long long
#define pb push_back
#define lb lower_bound
#define ub upper_bound
#define INF 0x3f3f3f3f
#define sf(x) scanf("%d",&x)
#define sc(x,y,z) scanf("%d%d%d",&x,&y,&z)
#define clr(x,n) memset(x,0,sizeof(x[0])*(n+5))
#define file(x) freopen(#x".in","r",stdin),freopen(#x".out","w",stdout)
using namespace std;
const int N = 6e5 + 10;
const int depth = 31;
struct Trie
{
#define ls T[x].ch[0]
#define rs T[x].ch[1]
void init(){tot=0;}
struct node{int size,ch[2],lazy;}T[N<<5];int tot;
int newnode(){memset(&T[++tot],0,sizeof(T[0]));return tot;}
void push_down(int x)
{
int lz=T[x].lazy;
if (lz&1) {swap(ls,rs);T[ls].lazy++;}
T[ls].lazy+=lz/2; T[rs].lazy+=lz/2;
T[x].lazy=0;
}
void ins(int &rt,int x)
{
int o=rt?rt:rt=newnode(),c;
for(int k=0;k<depth;k++)
{
c=x&1; x>>=1; T[o].size++;
if (T[o].lazy) push_down(o);
if (!T[o].ch[c]) T[o].ch[c]=newnode();
o=T[o].ch[c];
}
}
int query(int rt,int x,int y)
{
int o=rt;
for(int k=0;k<y;k++)
{
if (T[o].lazy) push_down(o);
o=T[o].ch[x&1]; x>>=1; if (!o) break;
}
return T[o].size;
}
void Merge(int x,int y)
{
T[x].size+=T[y].size;
if (T[x].lazy) push_down(x);
if (T[y].lazy) push_down(y);
for(int i=0;i<2;i++)
{
if (T[x].ch[i]&&T[y].ch[i]) Merge(T[x].ch[i],T[y].ch[i]);
if (!T[x].ch[i]&&T[y].ch[i]) T[x].ch[i]=T[y].ch[i];
}
}
} Trie;
int n,m,rt[N],f[N];
int find(int x)
{
return f[x]==x?x:f[x]=find(f[x]);
}
int main()
{
while(~sf(n))
{
sf(m);
clr(rt,n);
Trie.init();
for(int i=1;i<=n;i++)
{
f[i]=i;
int x; sf(x);
Trie.ins(rt[i],x);
}
while(m--)
{
int op; sf(op);
if (op==1)
{
int x,y;
sf(x); sf(y);
x=find(x); y=find(y);
if (x!=y)
{
Trie.Merge(rt[x],rt[y]); f[y]=x;
}
}
if (op==2)
{
int x; sf(x);
Trie.T[rt[find(x)]].lazy++;
}
if (op==3)
{
int x,y,z; sc(x,y,z); x=find(x);
printf("%d\n",Trie.query(rt[x],z,y));
}
}
}
return 0;
}