Loading

文艺平衡树

前置知识:普通平衡树。

实际上,平衡树能做到的操作不只有插入删除,查排名查值,前驱后继这些。
如果我们把平衡树移到区间上,我们甚至能够支持比线段树更强的操作——区间翻转。
这就是文艺平衡树这道题了。

1. 区间树

首先我们来明确一下区间树的概念。
顾名思义,区间树就是用来维护区间的。
具体地,我们规定平衡树的中序遍历得到的数列就是我们所维护的数组。
比如下面这一棵树:

中序遍历得 \(1\ 3\ 5\ 4\),也就是说这棵平衡树就维护了 \(1\ 3\ 5\ 4\) 这个数组。
需要注意的是,这里我们的平衡树只是用来维护数列的一个结构;可以看到,数列中的数本身是并不符合 BST 特性的。
或者说,满足 BST 特性的是数组下标,而不是数组中的值。

2. 思路

和线段树一样,我们也需要引用区间翻转的懒标记 rev。
具体操作的时候,我们将区间对应的这棵树提取出来(不同的数据结构有不同的做法),然后打上标记。
下传标记的时候,只需要交换左右子树,把标记传到子树上即可。

下传标记的代码:

inline void pushdown(int x)
{
    swap(tree[x].l,tree[x].r);//交换左右子树,是 l,r 还是 ch[0],ch[1] 依数据结构而定
    tree[tree[x].ch[0]].rev^=1;//下传标记,注意这个时候如果子树上也有标记那么两个标记就抵消了
    tree[tree[x].ch[1]].rev^=1;
    tree[x].rev=0;
}

3. Splay 实现文艺平衡树

上文说到,打标记时需要将区间对应的树提取出来。
在 Splay 上操作是这样的:
设区间为 \([l,r]\),那么我们先将 \(l-1\) 位置的结点 Splay 到根,再将 \(r+1\) 位置的结点 Splay 到 \(l-1\) 的子节点的位置上,那么此时 \(r+1\) 的左子树就代表了 \([l,r]\) 这个区间。
实际操作的时候,我们为了保证 \(l-1\)\(r+1\) 总是存在,我们会在开头和结尾插入结点表示位置 \(0\)\(n+1\)
这时我们要旋转的结点的排名就变为了 \(l\)\(r+2\),这是需要注意的。

放上图以便理解:
首先我们有一棵维护区间的 Splay(结点数字表示数组下标)。

这时我们要翻转区间 \([1,3]\)。所以我们要先把 0 Splay 到根:

再把 4 Splay 到 0 的子节点的位置上:

这个时候我们发现 4 的左子树就是我们要操作的 \([1,3]\) 了。

接下来上代码:

int n,m,tot,rt;
struct node{int fa,ch[2],pos,val,cnt,siz,rev;}tree[maxn];//这个地方多了一个量 pos 来代表数组下标,方便操作
inline bool dir(int x,int f){return tree[f].ch[1]==x;}
inline void con(int x,int f,int s){tree[f].ch[s]=x;tree[x].fa=f;}
inline void pushup(int x){tree[x].siz=tree[tree[x].ch[0]].siz+tree[tree[x].ch[1]].siz+tree[x].cnt;}
inline void newnode(int &x,int f,int pos,int val){x=++tot;tree[x]=(node){f,{0,0},pos,val,1,1,0};}//当然 newnode 也要跟着改
inline void rotate(int x)
{
    int f=tree[x].fa,g=tree[f].fa,k=dir(x,f);
    con(tree[x].ch[k^1],f,k);con(x,g,dir(f,g));con(f,x,k^1);
    pushup(f);pushup(x);
}
inline void splay(int x,int top)
{
    if(!top)rt=x;
    while(tree[x].fa!=top)
    {
        int f=tree[x].fa,g=tree[f].fa;
        if(g!=top)dir(x,f)^dir(f,g)?rotate(x):rotate(f);
        rotate(x);
    }
}
inline void pushdown(int x)//pushdown 操作,之前说过
{
    swap(tree[x].ch[0],tree[x].ch[1]);
    tree[tree[x].ch[0]].rev^=1;
    tree[tree[x].ch[1]].rev^=1;
    tree[x].rev=0;
}
void ins(int pos,int val,int &now,int fa)
{
    if(!now){newnode(now,fa,pos,val);splay(now,0);}
    else if(pos<tree[now].pos)ins(pos,val,tree[now].ch[0],now);//注意因为这是棵区间树,我们要按照 pos 进行排序
    else if(pos>tree[now].pos)ins(pos,val,tree[now].ch[1],now);
    else{tree[now].cnt++;splay(now,0);}
}
int getnum(int rk)
{
    int now=rt;
    while(1)
    {
        if(tree[now].rev)pushdown(now);//记得 pushdown
        int lsiz=tree[tree[now].ch[0]].siz;
        if(rk==lsiz+1)return now;
        else if(rk<=lsiz)now=tree[now].ch[0];
        else{rk-=lsiz+1;now=tree[now].ch[1];}
    }
}
void modify_rev(int l,int r)
{
    int x=getnum(l),y,z=getnum(r+2);//这个 l 和 r+2 的意义前面已经说过了
    //不要借助于 tree 的下标来确定 x 和 z,因为如果那样做中间的结点就没有 pushdown 了
    splay(x,0);splay(z,x);
    y=tree[tree[rt].ch[1]].ch[0];tree[y].rev^=1;
}
void tour(int x)//中序遍历输出结果
{
    if(!x)return;
    if(tree[x].rev)pushdown(x);//记得要 pushdown
    tour(tree[x].ch[0]);
    if(tree[x].pos!=0&&tree[x].pos!=n+1)printf("%d ",tree[x].val);//记得把插入的结点 0 和 n+1 去掉
    tour(tree[x].ch[1]);
}

4. fhq_treap 实现文艺平衡树

有了上面 Splay 的铺垫,那么 fhq_treap 的做法就相当简单了。
fhq_treap 通过按大小分裂来取出我们需要的区间。
具体做法是,先按顺序分出一个大小为 l-1 的树,再从余下的部分按顺序分出一个大小为 r-l+1 的树,这棵树就是我们要处理的区间。
按大小分裂的写法和按权值分裂区别不大。直接看代码:

inline void split(int x,int siz,int &rx,int &ry)//按顺序分裂成两棵树 rx 和 ry,使 rx 的大小为给定的 siz
{
    if(!x){rx=ry=0;return;}
    if(tree[x].rev)pushdown(x);//注意了,带标记的要 pushdown
    if(tree[tree[x].l].siz<siz){rx=x;split(tree[x].r,siz-tree[tree[x].l].siz-1,tree[x].r,ry);}//只不过是递归的时候要处理一下 siz 值
    else{ry=x;split(tree[x].l,siz,rx,tree[x].l);}
    pushup(x);
}

剩下的就没什么好说的了。注意 pushdown 就行。

inline int merge(int x,int y)
{
    if(!x||!y)return x+y;
    if(tree[x].key<tree[y].key)
    {
        if(tree[x].rev)pushdown(x);//要 pushdown
        tree[x].r=merge(tree[x].r,y);pushup(x);return x;
    }
    else
    {
        if(tree[y].rev)pushdown(y);
        tree[y].l=merge(x,tree[y].l);pushup(y);return y;
    }
}
void ins(int pos,int val)//为了方便按大小分裂,插入时多给一个参数 pos
{
    int now,x,y;
    split(rt,pos,x,y);newnode(now,val);
    rt=merge(merge(x,now),y);
}
void modify_rev(int l,int r)
{
    int x,y,z;
    split(rt,l-1,x,y);split(y,r-l+1,y,z);//前面说的两次分裂
    tree[y].rev^=1;
    rt=merge(merge(x,y),z);
}
void tour(int x)//中序遍历输出结果,别忘了 pushdown
{
    if(!x)return;
    if(tree[x].rev)pushdown(x);
    tour(tree[x].l);
    printf("%d ",tree[x].val);
    tour(tree[x].r);
}
posted @ 2022-02-09 00:01  pjykk  阅读(218)  评论(0编辑  收藏  举报