文艺平衡树¶
您需要写一种数据结构(可参考题目标题),来维护一个有序数列。
其中需要提供以下操作:翻转一个区间,例如原有序序列是 \(5\ 4\ 3\ 2\ 1\),翻转区间是 \([2,4]\) 的话,结果是 \(5\ 2\ 3\ 4\ 1\)。
我们以FHQ举例。
因为是区间的旋转,类似线段树,我们也需要一个懒标记
struct node{
int l,r,val,key,size,tag;
}tr[N];
我们还需要一个下传标记的东西。
我们需要在分裂、合并、输出时下传标记并更新。
void pushdown(int x){
if(!tr[x].tag)return ;
swap(tr[x].l,tr[x].r);//旋转子树
tr[tr[x].l].tag^=1;
tr[tr[x].r].tag^=1;//考虑旋转的性质传递tag,即奇数次翻转->要翻转.偶数次翻转->不需要翻转
tr[p].tag=0;
}
那么我们就会问了,这样写和线段树有什么区别呢?实际上,有一些旋转的题目确实可以用线段树做。我们考察线段树与平衡时的区别:平衡树的节点上存储的是数组中的一个值,而线段树存储的是一个区间的信息。
分裂¶
在平衡树模板中,我们按照当前节点的权值来进行分裂。而在文艺平衡树中,我们按照其排名来分裂。为什么呢?因为我们的文艺平衡树维护的是区间的翻转,区间对应着数组中的下标,而下标对应着每个点的排名。注意这里的排名不是值的大小排名,而是对平衡树进行中序遍历的排名。
void split(int i,int k,int &x,int &y){//i当前节点,v划分数值, 返回时x会指向左treap的根,y指向右treap的根
if(!i){//到达树叶
x=y=0;return ;
}
pushdown(i);
if(tr[tr[i].l].size<k){//如果这个点的val<=v,那么它的左子树一定都<=v,但是右子树的root虽然>v,但我们不知道它的儿子们是否都>v,所以需要递归右子树
k-=tr[tr[i].l].size+1;//走右子树,那么就要把排名减去左子树的大小.记得更新k
x=i;
split(tr[i].r,k,tr[i].r,y);//递归实现
}else{
y=i;
split(tr[i].l,k,x,tr[i].l);
}
pushup(i);
}
合并¶
加上pushdown即可。
int merge(int x,int y){// x,y分别是左右treap的根
if(!x||!y){
return x+y;
}
if(tr[x].key<tr[y].key){//如果x的key<y的key,那么y就作为x的子树,且是右子树,递归合并x原来的右子树和y
pushdown(x);
tr[x].r=merge(tr[x].r,y);
pushup(x);return x;
}else{
pushdown(y);
tr[y].l=merge(x,tr[y].l);
pushup(y);return y;
}
}
输出¶
用平衡树维护数组,输出应该使用中序遍历
void oup(int x){
//中序遍历来输出
if(!x)return ;
pushdown(x);//注意先下传标记,和线段树的query一样
oup(tr[x].l);
cout<<tr[x].val<<' ';
oup(tr[x].r);
}
操作 main()
¶
cin>>l>>r;
split(rt,r,x,z);//分裂为[1,r][r+1,n],根节点分别为x,z
split(x,l-1,x,y);//把[1,r]分裂为[1,l-1][l,r],根节点分别为x,y
//那么现在y子树就是[l,r]啦
tr[y].tag^=1;//注意不是=1
rt=merge(merge(x,y),z);
Code¶
/*////////ACACACACACACAC///////////
Code By Ntsc
文艺平衡树
/*////////ACACACACACACAC///////////
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+5;
struct node{
int l,r,val,key,size,tag;
}tr[N];
int n,m,root,idx; //root记录根的编号 ,idx是对新的节点进行编号的变量
int addnode(int v){
tr[++idx].val=v;
tr[idx].key=rand();
tr[idx].size=1;
return idx;//返回这个点在数组里的序号
}
void pushup(int i){//向上更新子树大小
tr[i].size=tr[tr[i].l].size+tr[tr[i].r].size+1;
}
void pushdown(int x){
if(!tr[x].tag)return ;
swap(tr[x].l,tr[x].r);//旋转子树
tr[tr[x].l].tag^=1;
tr[tr[x].r].tag^=1;//考虑旋转的性质传递tag,即奇数次翻转->要翻转.偶数次翻转->不需要翻转
tr[x].tag=0;
}
//分裂 根据v将树划分为2个子树
void split(int i,int k,int &x,int &y){//i当前节点,v划分数值, 返回时x会指向左treap的根,y指向右treap的根
if(!i){//到达树叶
x=y=0;return ;
}
pushdown(i);
if(tr[tr[i].l].size<k){//如果这个点的val<=v,那么它的左子树一定都<=v,但是右子树的root虽然>v,但我们不知道它的儿子们是否都>v,所以需要递归右子树
k-=tr[tr[i].l].size+1;//走右子树,那么就要把排名减去左子树的大小.记得更新k
x=i;
split(tr[i].r,k,tr[i].r,y);//递归实现
}else{
y=i;
split(tr[i].l,k,x,tr[i].l);
}
pushup(i);
}
//合并 分裂的逆过程.递归缝合2个分裂的treap
int merge(int x,int y){// x,y分别是左右treap的根
if(!x||!y){
return x+y;
}
if(tr[x].key<tr[y].key){//如果x的key<y的key,那么y就作为x的子树,且是右子树,递归合并x原来的右子树和y
pushdown(x);
tr[x].r=merge(tr[x].r,y);
pushup(x);return x;
}else{
pushdown(y);
tr[y].l=merge(x,tr[y].l);
pushup(y);return y;
}
}
void insert(int v){
int x,y,z;
split(root,v,x,y);
z=addnode(v);
root=merge(merge(x,z),y);//相当于z是一个1个节点的树,把它先和x合并(因为x的val均<=v,保证了它的大小顺序,至于它会被放在x的根或者其他地方,凭借key来确定),再和y合并
}
void del(int v){
int x,y,z;//将来会分别指向3棵树,他们的节点val分别是<v,=v,>v
split(root,v,x,z);//此时分成了2棵树,x指向的树是<=v的,y则是>v的
split(x,v-1,x,y);//再把x分成2棵树,把<v(x)的和=v(y)的拎出来
y=merge(tr[y].l,tr[y].r);//把y变成y的左右子树合并,相当于把根抛弃了
root=merge(merge(x,y),z);//重新合并
}
int getk(int i,int k){//获取中序排序第k个值的编号
if(k<=tr[tr[i].l].size)return getk(tr[i].l,k);//说明要找到点在左子树,那么去左子树找第k个
if(k==tr[tr[i].l].size+1)return i;//找到了
return getk(tr[i].r,k-tr[tr[i].l].size-1);//否则 说明要找到点在右子树,那么去左子树找第(k-size左子树)个(左子树已经有size个了,那么要找整个的第k个,只要找右子树的第(k-size左子树)个即可)
}
int getpre(int v){//找到v的前驱 (即<v的最大的那个点)
int x,y;//x,y只是暂时存放一下劈开的2个子树的根,后面还要合并
split(root,v-1,x,y);//劈开,变成<v(x)和>=v(y) 2个树
int p=getk(x,tr[x].size);//在子树x里面找到最后一个就是 <v(x)的最大的那个点
cout<<tr[p].val<<endl;
root=merge(x,y);//别忘了合并
}
int getsuc(int v){//找到v的后驱 (即>v的最小的那个点)
int x,y;
split(root,v,x,y);//劈开,变成<=v(x)和>v(y) 2个树
int p=getk(y,1);//在子树y里面找到第一个就是 >v(x)的最小的那个点
cout<<tr[p].val<<endl;//cout<<"OK";
root=merge(x,y);//别忘了合并
}
void getrank(int v){//查询val=v的点的排名(从小到大) 如果有重复的val=v的节点只计第一个,排序不去重
int x,y;
split(root,v-1,x,y);//劈开,变成<v(x)和>=v(y) 2个树
cout<<tr[x].size+1<<endl;//子树x的大小就是val=v的点前面有几个点
root=merge(x,y);
}
void getval(int k){//查询排名为k的节点的值
int p=getk(root,k);
cout<<tr[p].val<<endl;
}
void oup(int x){
//中序遍历来输出
if(!x)return ;
pushdown(x);
oup(tr[x].l);
cout<<tr[x].val<<' ';
oup(tr[x].r);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
root=merge(root,addnode(i));
}
for(int i=1;i<=m;i++){
int l,r;
int x,y,z;
cin>>l>>r;
split(root,r,x,z);//分裂为[1,r][r+1,n],根节点分别为x,z
split(x,l-1,x,y);//把[1,r]分裂为[1,l-1][l,r],根节点分别为x,y
//那么现在y子树就是[l,r]啦
tr[y].tag^=1;//注意不是=1
root=merge(merge(x,y),z);
}
oup(root);
return 0;
}
变形:区间修改平衡树¶
写一个支持区间加的平衡树
练习 | 这人怎么天天刷题啊(old) Hitchhiking in the Baltic States
/*////////ACACACACACACAC///////////
Code By Ntsc
文艺平衡树改
/*////////ACACACACACACAC///////////
void pushdown(int x){
if(!tr[x].tag)return ;
tr[tr[x].l].val+=tr[x].tag;
tr[tr[x].r].val+=tr[x].tag;
tr[tr[x].l].tag+=tr[x].tag;
tr[tr[x].r].tag+=tr[x].tag;//考虑旋转的性质传递tag,即奇数次翻转->要翻转.偶数次翻转->不需要翻转
tr[x].tag=0;
}
void changeadd(int l,int r,int v){
int x,y,z;
split(root,r,x,z);//分裂为[1,r][r+1,n],根节点分别为x,z
split(x,l-1,x,y);//把[1,r]分裂为[1,l-1][l,r],根节点分别为x,y
//那么现在y子树就是[l,r]啦
tr[y].tag+=v;
tr[y].val+=v;
root=merge(merge(x,y),z);
}