洛谷P6136 【模板】普通平衡树(数据加强版)
P6136 【模板】普通平衡树(数据加强版)
洛谷题目传送门
题目背景
本题是 P3369 数据加强版,扩大数据范围并增加了强制在线。
题目的输入、输出和原题略有不同,但需要支持的操作相同。
题目描述
您需要动态地维护一个可重集合 M M M,并且提供以下操作:
- 向 M M M 中插入一个数 x x x。
- 从 M M M 中删除一个数 x x x(若有多个相同的数,应只删除一个)。
- 查询 M M M 中有多少个数比 x x x 小,并且将得到的答案加一。
- 查询如果将 M M M 从小到大排列后,排名位于第 x x x 位的数。
- 查询 M M M 中 x x x 的前驱(前驱定义为小于 x x x,且最大的数)。
- 查询 M M M 中 x x x 的后继(后继定义为大于 x x x,且最小的数)。
本题强制在线,保证所有操作合法(操作 2 2 2 保证存在至少一个 x x x,操作 4 , 5 , 6 4,5,6 4,5,6 保证存在答案)。
输入格式
第一行两个正整数 n , m n,m n,m,表示初始数的个数和操作的个数。
第二行 n n n 个整数 a 1 , a 2 , a 3 , … , a n a_1,a_2,a_3,\ldots,a_n a1,a2,a3,…,an,表示初始的数。
接下来 m m m 行,每行有两个整数 opt \text{opt} opt 和 x ′ x' x′, opt \text{opt} opt 表示操作的序号($ 1 \leq \text{opt} \leq 6 ), ), ),x’$ 表示加密后的操作数。
我们记 last \text{last} last 表示上一次 3 , 4 , 5 , 6 3,4,5,6 3,4,5,6 操作的答案,则每次操作的 x ′ x' x′ 都要异或上 last \text{last} last 才是真实的 x x x。初始 last \text{last} last 为 0 0 0。
输出格式
输出一行一个整数,表示所有 3 , 4 , 5 , 6 3,4,5,6 3,4,5,6 操作的答案的异或和。
输入输出样例 #1
输入 #1
6 7
1 1 4 5 1 4
2 1
1 9
4 1
5 8
3 13
6 7
1 4
输出 #1
6
说明/提示
样例解释
样例加密前为:
6 7
1 1 4 5 1 4
2 1
1 9
4 1
5 9
3 8
6 1
1 0
限制与约定
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 5 1\leq n\leq 10^5 1≤n≤105, 1 ≤ m ≤ 1 0 6 1\leq m\leq 10^6 1≤m≤106, 0 ≤ a i , x < 2 30 0\leq a_i,x\lt 2^{30} 0≤ai,x<230。
本题输入数据较大,请使用较快的读入方式。
upd 2022.7.22 \text{upd 2022.7.22} upd 2022.7.22:新增加 9 9 9 组 Hack 数据。
题目讲解
由题目我们可以看出,这道题明显是平衡树
而平衡树有很多种,我们尽量选择功能强大,代码简单,便于理解的方法
不过这是一个模板题,选用很多方法都可以(主要是没有区间操作),你的带旋Treap固然很基础,
但还是太吃理解了(尤其是左旋和右旋,splay更不用说了),那有没有简单一点的方法呢?有的,兄
弟,当然有的,我们可以采用无旋Treap,这里我们来详细学习一下。
无旋Treap
基础概念
无旋Treap,又称FHQ,主要是依靠分裂(split)与合并(merge)来完成各种操作的
他和带旋Treap一样,同时具有BST性质与堆的性质
结构体定义
首先,val(值),priority(优先级),l(左子),r(右子)这几个是必须有的,仔细看这个题目,它还要求给
出排名,所以子树大小siz也是要有的。由于这里我把值相同的节点拆开成不同节点了(这样插入删除简单一点),也就不需要节点数量cnt了。
struct lit{ll val,priority,siz;lit *l,*r;lit(ll _val){val=_val,priority=rand(),siz=1,l=nullptr,r=nullptr;}
//新建节点,同带旋Treap一样,priority随机赋值
};
分裂(split)
如果我们想要分裂一棵树u,我们首先需要一个标准。这个标准可能是值val或子树大小siz。在这个题中,我们显然要以值val为标准,将一棵树分裂成两个部分x,y,其中 m a x ( x ) < = k e y < m i n ( y ) ( m a x , m i n 分别表示当前树中的最大,最小值 ) max(x)<=key<min(y)(max,min分别表示当前树中的最大,最小值) max(x)<=key<min(y)(max,min分别表示当前树中的最大,最小值),这样我们在插入和删除时就可以很好胜任了。这样分裂的树和合并时恰好契合。
具体的操作如下:
- 退出条件:如果u为空树,那分裂出的x,y也都为空树
- 如果u的值u->val<=val,那么说明u的子树u->l全部符合条件,另外u的右子树总还可能有节点<=val,我们要把这些节点剖出来,而且剖出之后只能放在x->r上。
- 否则u->r全部符合>key,同理,u->l上也可能还有节点>key,要将他们剖到y->l上
- 记得在任意改变了树的结构后都有更新树的大小
ll getsize(lit *p){return p?p->siz:0;}void updatasize(lit *&p){if(p){p->siz=getsize(p->l)+getsize(p->r)+1;}}void split(lit *u,ll val,lit *&x,lit *&y){
//依照val分裂以 u 为根的子树,将他分裂到x(x<=val),y,(y>val)if(!u){x=y=nullptr;return;}
//空树分裂后仍然是空树if(u->val<=val){x=u;split(u->r,val,x->r,y);updatasize(x);}
//u的左树都小于key,将这一节接到 x 后,u的右子树上也还有可能有值<val
//但由于这些值都大于u的任意左子节点,只能接到x的右子树上else{y=u;split(u->l,val,x,y->l);updatasize(y);}//同理}
合并(merge)
对于合并操作,我们给定两棵树x,y,保证 m a x ( x ) < m i n ( y ) max(x)<min(y) max(x)<min(y),这样才能保证合并后的树满足BST性质,我们发现合并的条件与分裂后产生的两棵树相同,他们可以看作互逆操作。
那么有两种情况:
- 将y拼到x的右子树上
- 将x拼到y的左子树上
那么合并的思路是:
- 如果两棵树中有一棵为空树,就返回另一棵树(都为空树的也包括在内)
- 为了满足堆的性质,如果x->priority>y->priotity,那么为第 1 种情况
- 否则为第 2 种情况
lit *merge(lit *x,lit *y){
//返回两颗树x,y合并的结果,且max(x)<min(y),此时要么将x拼到y的左子上,要么将y拼到x的右子上if(!x)return y;if(!y)return x;
//两个都为空树或一个为空树,直接返回另一个if(x->priority>y->priority){x->r=merge(x->r,y);updatasize(x);return x;}
//维护堆的性质:父节点的优先级大于其子节点
//x的优先级高就让x当父节点(将y拼到x的右子上)else{y->l=merge(x,y->l);updatasize(y);return y;}//同理可得
}
加入节点(insert)
对于加入节点,我们只需要将原树分开,在将我们的节点加入进去就可以了
void insert(ll val){//插入一个值为val的节点lit *x,*y;split(root,val,x,y);
//先将这棵树分裂,然后直接加入这个节点root=merge(merge(x,new lit(val)),y);
//注意顺序不能变,因为max(x)<val<min(y)}
删除节点(remove)
如果我们想要删除一个值为val的节点,我们只需要将树分为三份:l,mid,r,其中
l < v a l , m i d = v a l , r > v a l l<val,mid=val,r>val l<val,mid=val,r>val,由此我们只需要删除mid中的一个节点即可。想要删除一个节点,那我们直接将左右子树合并,这样就少了根节点。
void remove(ll val){
//删除一个值为val的节点,只需要将树分为三部分
//分别为x(<val),y(=val),z(>val),然后只需要将y删除一个节点就可以了lit *x,*y,*z;split(root,val,x,z);//此时x<=val,z>valsplit(x,val-1,x,y);//此时x<val,y=valif(y){//如果有,就删除一个节点lit *tmp=y;y=merge(y->l,y->r);//将当前节点改为左右合并的结果,这样就少了当前节点delete tmp;}root=merge(merge(x,y),z);//重新合并(注意顺序)}
注意:对于这种区间有关的操作,我建议先将右边扣去,在扣去左边,这样便不会出现左右的偏差,比如取出区间 [ l , r ] [l,r] [l,r],我们先取出区间 [ 1 , r ] [1,r] [1,r],再取出区间 [ l , r ] [l,r] [l,r]
其他操作
其他操作我在这里简述一下:
- 求前缀(pre):我们只需将树分为x<val,y>=val,再取x中的最大值即可
- 求后缀(suc):同理,将树分为x<=val,y>val,再取y中的最小值
- 求排名(rank):及求有多少个节点比val小,在加上1(自己)
- 求排名为k的节点(kth):与二叉搜索树一样,递归求解
lit *getpre(lit *p,ll val){//求前驱,即求最后一个小于key的节点lit *x,*y;split(root,val-1,x,y);//此时max(x)<vallit *u=x;while(u&&u->r)u=u->r;//能往右边走就往右边走root=merge(x,y);//记得合并return u;}lit *getsuf(lit *p,ll val){lit *x,*y;split(root,val,x,y);//此时min(y)>vallit *u=y;while(u&&u->l)u=u->l;//能往右边走就往右边走root=merge(x,y);//记得合并return u;}ll getrank(lit *p,ll val){//求排名,即求 有多少个节点比key小+1lit *x,*y;split(root,val-1,x,y);//此时x<valint res=getsize(x)+1;root=merge(x,y);//记得合并return res;}lit *getkth(lit *p,ll x){while(p){ll l=getsize(p->l);if(x<=l)p=p->l;//目标在左子树上else if(x>l+1){x-=l+1;p=p->r;}//目标在右子树上,右子树排名有变动,要将前面的节点去掉else return p;//恰好为当前节点}return nullptr;//没有}
完整代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
struct lit{ll val,priority,siz;lit *l,*r;lit(ll _val){val=_val,priority=rand(),siz=1,l=nullptr,r=nullptr;}
//新建节点,同带旋Treap一样,priority随机赋值
};
class FHQ_Treap{
private:lit *root;ll getsize(lit *p){return p?p->siz:0;}void updatasize(lit *&p){if(p){p->siz=getsize(p->l)+getsize(p->r)+1;}}void split(lit *u,ll val,lit *&x,lit *&y){
//分裂依照key以 u 为根的子树,将他分裂到x(x<=val),y,(y>val)if(!u){x=y=nullptr;return;}
//空树分裂后仍然是空树if(u->val<=val){x=u;split(u->r,val,x->r,y);updatasize(x);}
//u的左树都小于key,将这一节接到 x 后,u的右子树上也还有可能有值<val
//但由于这些值都大于u的任意左子节点,只能接到x的右子树上else{y=u;split(u->l,val,x,y->l);updatasize(y);}//同理}lit *merge(lit *x,lit *y){
//返回两颗树x,y合并的结果,且max(x)<min(y),,此时要么将x拼到y的左子上,要么将y拼到x的右子上if(!x)return y;if(!y)return x;
//两个都为空树或一个为空树,直接返回另一个if(x->priority>y->priority){x->r=merge(x->r,y);updatasize(x);return x;}
//维护堆的性质:父节点的优先级大于其子节点
//x的优先级高高就让x当父节点(将y拼到x的右子上)else{y->l=merge(x,y->l);updatasize(y);return y;}//同理可得}lit *getpre(lit *p,ll val){//求前驱,即求最后一个小于key的节点lit *x,*y;split(root,val-1,x,y);//此时max(x)<vallit *u=x;while(u&&u->r)u=u->r;//能往右边走就往右边走root=merge(x,y);//记得合并return u;}lit *getsuf(lit *p,ll val){lit *x,*y;split(root,val,x,y);//此时min(y)>vallit *u=y;while(u&&u->l)u=u->l;//能往右边走就往右边走root=merge(x,y);//记得合并return u;}ll getrank(lit *p,ll val){//求排名,即求 有多少个节点比key小+1lit *x,*y;split(root,val-1,x,y);//此时x<valint res=getsize(x)+1;root=merge(x,y);//记得合并return res;}lit *getkth(lit *p,ll x){while(p){ll l=getsize(p->l);if(x<=l)p=p->l;//目标在左子树上else if(x>l+1){x-=l+1;p=p->r;}//目标在右子树上,右子树排名有变动,要将前面的节点去掉else return p;//恰好为当前节点}return nullptr;//没有}
public:void insert(ll val){//插入一个值为val的节点lit *x,*y;split(root,val,x,y);
//先将这棵树分裂,然后直接加入这个节点root=merge(merge(x,new lit(val)),y);
//注意顺序不能变,因为max(x)<val<min(y)}void remove(ll val){
//删除一个值为val的节点,只需要将树分为三部分
//分别为x(<val),y(=val),z(>val),然后只需要将y删除一个节点就可以了lit *x,*y,*z;split(root,val,x,z);//此时x<=val,z>valsplit(x,val-1,x,y);//此时x<val,y=valif(y){//如果有,就删除一个节点lit *tmp=y;y=merge(y->l,y->r);//将当前节点改为左右合并的结果,这样就少了当前节点delete tmp;}root=merge(merge(x,y),z);//重新合并(注意顺序)}lit *pre(ll x){return getpre(root,x);}lit *suf(ll x){return getsuf(root,x);}ll rank(ll x){return getrank(root,x);}lit *kth(ll x){return getkth(root,x);}
}tr;
ll n,m,ans=0,pre=0;
int main(){ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
// cin>>n;
// for(int i=1;i<=n;i++){int x;cin>>x;tr.insert(x);}
// cout<<tr.kth(4)->val<<'\n';cin>>n>>m;for(int i=1;i<=n;i++){ll x;cin>>x;tr.insert(x);}while(m--){ll op,x;cin>>op>>x;x=pre^x;
// tr.print();
// cout<<op<<' '<<x<<'\n';if(op==1)tr.insert(x);if(op==2)tr.remove(x);if(op==3){pre=tr.rank(x);}if(op==4){pre=tr.kth(x)->val;}if(op==5){pre=tr.pre(x)->val;}if(op==6){pre=tr.suf(x)->val;}if(op>=3)ans=ans^pre;//细节,有操作才更新}cout<<ans;return 0;
}