平衡树总结

高级数据结构_平衡树学习笔记

本文章同步与我的Luogu博客。



学习平衡树的一些总结。   



BST树,即二叉查找树,是一种数据结构,满足这样的条件:
一颗BST树的中序遍历是有序的。
例:
将{1,4,2,3,5,8,10,7}建成BST树:
平衡树总结_第1张图片

某同学:
BST树长得又丑,又不能节省空间,我干嘛要学它啊。

因为能省时间。
我们找到任意节点p,发现p的左子树中的任意节点都小于p,p的右子树中的任意节点都大于p
有了这个性质,我们就可以来看一些关键操作了。
1.查找:
在BST上查找一个元素类似于二分查找。
设当前元素大小为q,带查找元素大小为k。

1.若k==q,直接返回
2.若k>q,向右子树递归
3.若k

期望复杂度O(logn)。

int search(int k,int u)
{
	if(!u)return 0;
	if(k==val(u))return u;
	if(k>val(u))return search(rs(u));
	if(k

简单明快。
2.插入:
插入也很简单,就是一个查找加上一个开点。

void new_point(int k,int u,int fa)
{
	if(!tot){a[++tot]=node(0,0,k);root=tot;}
	if(!u){a[++tot]=node(0,0,k);if(k>val(fa))rs(fa)=tot;else ls(fa)=tot;return;}
	if(k>val(u))new_point(k,rs(u),u);
	if(k<val(u))new_point(k,ls(u),u);
}

3.删除:
也简单,删除值为x的点也是一次查找…
找不到就作罢,找到了的话就分三种情况讨论。
1.当前点没有左右孩子,直接将其父亲的这个孩子清空。

2.当前点只有一个孩子,将那个孩子提到当前位置。

3.当前点有两个孩子,就将其左子树中的最大点置换到当前点的位置。

void del_point(int k,int u,int fa)
{
	if(!u)return;
	if(k==val(u))
	{
		if(!ls(u)&&!rs(u))if(k>val(fa))rs(fa)=0;else ls(fa)=0;
		if(!ls(u))if(k>val(fa))rs(fa)=rs(u);else ls(fa)=rs(u);
		if(!rs(u))if(k>val(fa))rs(fa)=ls(u);else ls(fa)=ls(u);
		else
		{
			p=ls(u);while(rs(rs(p)))p=rs(p);
			if(root==u)root=p;int fi=p;p=rs(p);
			rs(fi)=ls(p);if(k>val(fa))rs(fa)=p;else ls(fa)=p;
		}
	}
	if(k>val(u))del_point(k,rs(u),u);
	if(k<val(u))del_point(k,ls(u),u);
}



我们知道,在树上操作的的时间复杂度多取决于树高
正是因为如此,我们可以说我们的BST树的期望时间复杂度是O(logn)。
但是我们可以考虑如下情况:
向一颗BST树顺序插入{1,2,3,4,5,6,7,8}。
得到的树长这样:

这棵树退化为了一条
我们又不能决定输入顺序,怎么办呢?
这里介绍一个黑科技:AVL树
AVL树就是我们常说的"高度平衡的树"。
我们引入一个概念:节点的平衡因子
一个节点的平衡因子是它的左子树高减去它的右子树高
AVL树是一颗空树或所有节点的平衡因子都为{-1,0,1}这三个数中的一个的一棵树。
这样就可以控制树高在logn附近。
怎么实现呢?
我们先考虑向一颗AVL中插入一个节点。

如图,插入元素导致一条链上的所有节点的平衡因子发生了变化。
上图是一个比较好的情况,插入元素没有对AVL树本身产生什么大的影响。
看下图:

这个时候,我们就考虑调整整棵树的结构来保持这一颗树的平衡性质。
调整的方法被称为平衡化旋转

通过平衡化旋转,我们可以在不破坏二叉搜索树性质的前提下维持树的平衡性。
平衡化旋转只跟一个点沿修改的链向下两个点有关。
怎么转呢?我们来分类讨论一下:
1.三点一线向左:

2.三点一线向右:

3.中间点向左突出:

4.中间点向右突出:

左单旋:

void lrotate(int x,int y,int z)
{
	int k=ls(y);
	ls(y)=x;
	rs(x)=k;
}

右单旋:

void rrotate(int x,int y,int z)
{
	int k=rs(y);
	rs(y)=x;
	ls(x)=k;
}

左右双旋:

void lrrotate(int x,int y,int z)
{
	ls(x)=z;
	rs(y)=ls(z);
	ls(z)=y;
	rrotate(x,z,y);
}

右左双旋:

void rlrotate(int x,int y,int z)
{
	rs(x)=z;
	ls(y)=rs(z);
	rs(z)=y;
	lrotate(x,z,y);
}

在每一个函数末尾都添加上修复用函数:

void fix_up(int x,int y,int z)
{
	if(x>y&&y>z)rrotate(x,y,z);
	if(x<y&&y<z)lrotate(x,y,z);
	if(x>z&&z>y)lrrotate(x,y,z);
	if(x<z&&z<y)rlrotate(x,y,z);
}

平衡化旋转的实质就是压缩一条链的高度来改进搜索树的结构。




Splay树,又名伸展树是一种自平衡树
严格上来说,splay树并不算是一棵高度平衡的树,但是它通过伸展操作(splay操作)来保持结构的高效性。
"伸展操作"的意思,是将一个特定的点转到特定的位置去。
一般用splay(x,T)代表将元素x旋转到splay树T的根结点处。
那么splay操作是怎么实现的呢?
还是基于平衡化旋转
不过这个旋转不再是为了使树变得平衡,而是为了改变的节点的深度。
有两种基础的旋转:
Zig(右旋)和Zag(左旋)。
Zig:
平衡树总结_第2张图片
Zag:
平衡树总结_第3张图片
这就是最基本的伸展树上的平衡化旋转。
接下来我们就要介绍怎么将一个节点旋转到根上了。
进行分类讨论:
情况1:(Zig单旋)待旋转节点为x,其父亲y为树根,x为y的左孩子。
这个时候进行一次Zig(x),就可以将x旋转至根上。

情况2:(Zag单旋)待旋转节点为x,其父亲y为树根,x为y的右孩子。
这个时候进行一次Zig(x),就可以将x旋转至根上。

1,2互为逆操作。
情况3:(Zig-Zig双旋)待旋转节点x的父亲(y),祖父(z)同向左偏斜。
进行一次Zig(y),一次Zig(x),将x旋转到z处。

情况4:(Zag-Zag双旋)待旋转节点x的父亲(y),祖父(z)同向右偏斜。
进行一次Zag(y),一次Zag(x),将x旋转到z处。

3,4互为逆操作。
情况5:(Zig-Zag双旋)待旋转节点x是其父亲y的左孩子,y是其父亲z的右孩子。
进行一次Zig(x),一次Zag(x),将x旋转到z处。

情况6:(Zag-Zig双旋)待旋转节点x是其父亲y的右孩子,y是其父亲z的左孩子。
进行一次Zag(x),一次Zig(x),将x旋转到z处。

附加内容:用一个rotate函数代替Zig和Zag两个函数。
首先定义一个connect函数,规定connect(x,y,son)代表将x连接到y的son孩子上。

bool identify(int p){return p==rs(fa(p));}
void connect(int p,int f,int son){fa(p)=f;prp(f,son)=p;}

我们发现,Zig(x)中的x必定是左孩子,Zag(x)中的x必定是右孩子。
所以我们就可以用rotate(x)来代替左右旋转了。


void rotate(int p)
{
    int y=fa(p);int mroot=fa(y);
    int mson=identify(y);
    int yson=identify(p);
    int z=lft(p,yson);
    connect(z,y,yson);
    connect(y,p,yson^1);
    connect(p,mroot,mson);
    push_up(y);push_up(p);
}

有了前述知识,splay操作就很好理解了。

void splay(int p,int to)
{
    to=fa(to);
    while(sp[p].fa!=to)
    {
        int up=fa(p);
        if(fa(up)==to)rotate(p);
        else if(identify(p)==identify(up)){rotate(up);rotate(p);}
        else{rotate(p);rotate(p);}
    }
}

例题:
Luogu P3369 【模板】普通平衡树
来看看这个例题要我们实现的几个操作:
1.插入:
splay的插入就和普通的insert操作一样,只是需要将新加入的节点旋转到根上。
图示:

2.删除:
splay的删除就比较的玄学了。
简单来说,就是找到要删除的点(就是二叉查找树的搜索),将其旋转到根上,将其直接删除掉,再将其的左右孩子合并。
图示:

void push(int val){int add=insert(val);splay(add,root);}
int insert(int val)
{
    if(tot==0){root=1;new_point(val,0);}
    else
    {
        int p=root;
        while(1)
        {
            sum(p)++;
            if(val==val(p)){cnt(p)++;return p;}
            int next=val<sp[p].val?0:1;
            if(!prp(p,next))
            {
                new_point(val,p);
                prp(p,next)=tot;
                return tot;
            }
            p=sp[p].ch[next];
        }
    }
    return 0;
}

3.查询某个数排名:
查询一个节点的排名,就是基本操作了。
简单来说,就是一次查找,找到比它大的就跳,找到比它小的就叠加size,找到一样的就输出。
记得查到结果了就splay一下,将节点旋到根。

int rank(int val) 
{
	sum(0)=0;
	int ans=0,p=root;
	while(1)
	{
		if(val(p)==val){int res=ans+sum(ls(p))+1;splay(p,root);return res;}
		if(p==0) return 0;
		if(val<val(p)) p=ls(p);
		else{ans=ans+sum(ls(p))+cnt(p);p=rs(p);}
	}
}

4.查询某个排名对应的数:
也是利用size域完成的。
,size够就向这一边跳,不够就往另一边,size符合就返回。

int atrank(int x) 
{
	int p=root;
	while(1)
	{
		int minused=sum(p)-sum(rs(p));
		if(x>sum(ls(p))&&x<=minused)break;
		if(x<minused)p=ls(p);
		else{x=x-minused;p=rs(p);}
	}
	splay(p,root);
	return val(p);
}

5,6.查询前驱&&后继:
很简单。
将插入一个值为待查询值的点,将其splay到根上去,输出完了就删掉。

int lower(){int p=ls(root);while(rs(p))p=rs(p);return val(p);}
int upper(){int p=rs(root);while(ls(p))p=ls(p);return val(p);}

例题代码:

#include
#include
#include
#include
#include
#include
#include
#include
#define root sp[0].ch[1]
#define ls(p) sp[p].ch[0]
#define rs(p) sp[p].ch[1]
#define val(p) sp[p].val
#define sum(p) sp[p].sum
#define cnt(p) sp[p].cnt
#define fa(p) sp[p].fa
#define prp(p,x) sp[p].ch[x]
#define lft(p,x) sp[p].ch[x^1]
using namespace std;
const int INF=2147480000;
struct node
{
    int ch[2],val,fa,sum,cnt;
}sp[100001];
void push_up(int p){sum(p)=sum(ls(p))+sum(rs(p))+cnt(p);}
bool identify(int p){return p==rs(fa(p));}
void connect(int p,int f,int son){fa(p)=f;prp(f,son)=p;}
void rotate(int p)
{
    int y=fa(p);int mroot=fa(y);
    int mson=identify(y);
    int yson=identify(p);
    int z=lft(p,yson);
    connect(z,y,yson);
    connect(y,p,yson^1);
    connect(p,mroot,mson);
    push_up(y);push_up(p);
}
void splay(int p,int to)
{
    to=fa(to);
    while(sp[p].fa!=to)
    {
        int up=fa(p);
        if(fa(up)==to)rotate(p);
        else if(identify(p)==identify(up)){rotate(up);rotate(p);}
        else{rotate(p);rotate(p);}
    }
}
int tot;
int new_point(int val,int fa){tot++;val(tot)=val;cnt(tot)=sum(tot)=1;fa(tot)=fa;return tot;}
void del_point(int p){fa(p)=ls(p)=rs(p)=fa(p)=sum(p)=cnt(p)=0;}
int find(int x)
{
    int p=root;
    while(1)
    {
        if(val(p)==x){splay(p,root);return p;}
        if(val(p)>x)p=ls(p);
        else p=rs(p);
        if(!p)return 0;
    }
}
int insert(int val)
{
    if(tot==0){root=1;new_point(val,0);}
    else
    {
        int p=root;
        while(1)
        {
            sum(p)++;
            if(val==val(p)){cnt(p)++;return p;}
            int next=val<sp[p].val?0:1;
            if(!prp(p,next))
            {
                new_point(val,p);
                prp(p,next)=tot;
                return tot;
            }
            p=sp[p].ch[next];
        }
    }
    return 0;
}
void push(int val){int add=insert(val);splay(add,root);}
void pop(int val)
{
    int p=find(val);
    if(!p)return;
    if(cnt(p)>1){cnt(p)--;sum(p)--;return;}
    if(!ls(p)){root=rs(p);fa(root)=0;}
    else
    {
        int lef=ls(p);
        while(rs(lef))lef=rs(lef);
        splay(lef,ls(p));
        int rig=rs(p);
        connect(rig,lef,1);
        connect(lef,0,1);
        push_up(lef);
    }
    del_point(p);
}
int rank(int val) 
{
    sum(0)=0;
    int ans=0,p=root;
    while(1)
    {
        if(val(p)==val){int res=ans+sum(ls(p))+1;splay(p,root);return res;}
        if(p==0) return 0;
        if(val<val(p)) p=ls(p);
        else{ans=ans+sum(ls(p))+cnt(p);p=rs(p);}
    }
}
int atrank(int x) 
{
    int p=root;
    while(1)
    {
        int minused=sum(p)-sum(rs(p));
        if(x>sum(ls(p))&&x<=minused)break;
        if(x<minused)p=ls(p);
        else{x=x-minused;p=rs(p);}
    }
    splay(p,root);
    return val(p);
}
int lower(){int p=ls(root);while(rs(p))p=rs(p);return val(p);}
int upper(){int p=rs(root);while(ls(p))p=ls(p);return val(p);}
int main()
{
    int n;sp[0].val=-INF;
    scanf("%d",&n);
    while(n--)
    {
        int op,x;
        scanf("%d%d",&op,&x);
        if(op==1) push(x);
        else if(op==2)pop(x);
        else if(op==3)printf("%d\n",rank(x));
        else if(op==4)printf("%d\n",atrank(x));
        else if(op==5){push(x);printf("%d\n",lower());pop(x);}
        else{push(x);printf("%d\n",upper());pop(x);}
    }
    return 0;
}



Think Functional!

要学FHQ Treap,首先要理解什么是FHQ
所谓的"FHQ",就是非旋的意思。
代表这种平衡树不需要依靠旋转来保持平衡。
事实上,FHQ Treap依靠分裂和合并来维持平衡性质。
所以我们称FHQ Treap为函数式Treap(即:不对现有的数据进行任何修改,仅仅是用历史数据来计算出新的)。
下面就来介绍一下这两个操作:
1.分裂:
所谓的"分裂",就是将一颗FHQ Treap分成两颗,每一颗都满足FHQ Treap的性质。
其实很简单,就是一个递归求解的过程。
定义split(p,k,&x,&y),代表将以p为根的子树分为两个部分,其根分别为x,y。
这种分裂应满足:
以x为根的FHQ Treap的size不大于k。
图例:

void split(int p,int k,int &x,int &y)
{
	if(!p){x=y=0;return;}
	push_down(p);
	if(tls(p).size<k){x=p;split(rs(x),k-tls(p).size-1,rs(x),y);}
	else{y=p;split(ls(y),k,x,ls(y));}
	push_up(p);
}

2.合并:
所谓的合并,就是将两颗FHQ Treap合为一颗。
也是一个递归的过程。
规定merge(x,y)返回的是将根为x,y的两棵树合并后的根。
图例:

int merge(int x,int y)
{
	if(!x||!y)return x+y;
	push_down(x);push_down(y);
	if(t(x).rnd<t(y).rnd){rs(x)=merge(rs(x),y);push_up(x);return x;}
	else{ls(y)=merge(x,ls(y));push_up(y);return y;}
}

例题:
Luogu P3391 【模板】文艺平衡树(Splay)
什么?你说题目要我们用Splay?不存在的。
就用FHQ Treap。
怎么支持区间翻转呢?
打标记,标记下传时swap左右子树。
代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
#define INF 0x3f3f3f3f
#define ls(p) tree[p].lson
#define rs(p) tree[p].rson
#define tls(p) tree[ls(p)]
#define trs(p) tree[rs(p)]
#define t(p) tree[p]
#define tpi t(++tot)
#define tp t(tot)
using namespace std;
const int N(2e5);
int n;
int root;
struct node
{
	int rnd,size,rev;
	int val;
	ll sum;
	int lson,rson;
}tree[N+10];
inline int new_node(long long v=0)
{
	static int tot(0);
	tpi.val=v;tp.sum=v;
	tp.rnd=rand();tp.size=1;
	return tot;
}
inline void push_up(int p)
{
	tree[p].size=tls(p).size+trs(p).size+1;
	tree[p].sum=tls(p).sum+trs(p).sum+t(p).val;
}
inline void push_down(int p)
{
	if(!t(p).rev)return;
	swap(ls(p),rs(p));
	if(ls(p))tls(p).rev^=1;
	if(rs(p))trs(p).rev^=1;
	tree[p].rev=0;
}
void split(int p,int k,int &x,int &y)
{
	if(!p){x=y=0;return;}
	push_down(p);
	if(tls(p).size<k){x=p;split(rs(x),k-tls(p).size-1,rs(x),y);}
	else{y=p;split(ls(y),k,x,ls(y));}
	push_up(p);
}
int merge(int x,int y)
{
	if(!x||!y)return x+y;
	push_down(x);push_down(y);
	if(t(x).rnd<t(y).rnd){rs(x)=merge(rs(x),y);push_up(x);return x;}
	else{ls(y)=merge(x,ls(y));push_up(y);return y;}
}
void outpt(int u)
{
    if(!u)return;
    if(t(u).rev)push_down(u);
    outpt(ls(u));
    printf("%d ",t(u).val);
    outpt(rs(u));
}
int main()
{
	int n,m;
	int x,y,a,b,c;
	srand(224144);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)root=merge(root,new_node(i));
	while(m--)
	{
		scanf("%d%d",&x,&y);
		split(root,x-1,a,b);
		split(b,y-x+1,b,c);
		t(b).rev^=1;
		root=merge(a,merge(b,c));
	}
	outpt(root);
	return 0;
}



这是一个题目:
Luogu P5055 【模板】可持久化文艺平衡树

是时候展现一下FHQ Treap的能力了


对于我们的所求,这一题的题目已经说的很清楚了。
在题解的正题开始之前,先放几个链接:
Luogu P3369 普通平衡树
Luogu P3391 文艺平衡树(Splay)
Luogu P3835 可持久化平衡树
建议试着用FHQ Treap(非旋转Treap)来实现这几题。




你看啊,这个数据结构的常数比Splay小,理解起来比Splay容易,长得比Splay好看,能实现的东西并不比Splay少,代码量比Splay小,还能可持久化,为什么不学学呢?

会FHQ Treap的可以跳过了。
所谓的FHQ Treap其实是一种加强版的Treap。与一般的Treap树不同,FHQ Treap不依赖旋转操作保持自身结构的平衡,而是依赖分裂合并操作维持树的平衡性质。
我们先来介绍一下关键操作:
1.创建新的节点(new_node):
很简单,就是创建一个新的点,没什么好说的。
返回当前点的下标。

inline int new_node(long long v=0)
{
	static int tot(0);
	tpi.val=v;tp.sum=v;
	tp.rand=rand();tp.size=1;
	return tot;
}

2.复制节点(copy_node):
也没什么好说的,仅仅是为了方便。
返回复制后的点的下标。

inline int copy_node(int p)
{
	int ret=new_node();
	tree[ret]=tree[p];
	return ret;
}

3.更新(push_up):
push_up§代表更新下标为p的节点。

inline void push_up(int p)
{
	tree[p].size=tls(p).size+trs(p).size+1;
	tree[p].sum=tls(p).sum+trs(p).sum+t(p).val;
}

4.标记下传(push_down):
push_down§代表将下标为p的点的标记下传。
什么标记呢?自然是翻转标记。
注意:传之前的点别扔了,留着可持久化呢。

inline void push_down(int p)
{
	if(!t(p).tag)return;
	if(ls(p))ls(p)=copy_node(ls(p));
	if(rs(p))rs(p)=copy_node(rs(p));
	swap(ls(p),rs(p));
	if(ls(p))tls(p).tag^=1;
	if(rs(p))trs(p).tag^=1;
	tree[p].tag=0;
}

5.分裂(Split):
这个词我经常打成Spilt
所谓的"分裂",就是将一颗Treap分成两部分。
你可以理解成,你拿着一个选择性透过膜来"过滤"一颗Treap的过程,最后会将一颗Treap过滤成两个部分。
我们定义split(p,k,x,y)代表将根为p的子树分为两部分,其中的一部分size为k。
具体实现起来就是左子树的size还够用的时候,就往左子树递归,不够用的话就往右子树递归。
先推标记,再分裂!!!
Split操作完整代码:

void split(int p,int k,int &x,int &y)
{
	if(!p){x=y=0;return;}
	push_down(p);
	if(tls(p).size<k){x=copy_node(p);split(rs(x),k-tls(p).size-1,rs(x),y);push_up(x);}
	else{y=copy_node(p);split(ls(y),k,x,ls(y));push_up(y);}
}

6.合并(Merge):
合并就更好理解了,就是把两棵子树树合并到一个根节点上。
跟一般的平衡树一样,我们需要以它们的键值大小关系决定怎么合并它们。(键值怎么得到?rand()了解一下)
返回值为他们的根节点。
先推标记,再合并!!!

int merge(int x,int y)
{
	if(!x||!y)return x|y;
	push_down(x);push_down(y);
	if(t(x).rand<t(y).rand){rs(x)=merge(rs(x),y);push_up(x);return x;}
	else{ls(y)=merge(x,ls(y));push_up(y);return y;}
}

以下是实现一颗可以拿去持久化的FHQ Treap的代码:

const int N(2e5);
int n;ll lastans;
struct node
{
	int rand,size,tag;
	ll val,sum;
	int lson,rson;
}tree[(N<<7)+10];
int rt[N+10];
inline int new_node(long long v=0)
{
	static int tot(0);
	tpi.val=v;tp.sum=v;
	tp.rand=rand();tp.size=1;
	return tot;
}
inline int copy_node(int p)
{
	int ret=new_node();
	tree[ret]=tree[p];
	return ret;
}
inline void push_up(int p)
{
	tree[p].size=tls(p).size+trs(p).size+1;
	tree[p].sum=tls(p).sum+trs(p).sum+t(p).val;
}
inline void push_down(int p)
{
	if(!t(p).tag)return;
	if(ls(p))ls(p)=copy_node(ls(p));
	if(rs(p))rs(p)=copy_node(rs(p));
	swap(ls(p),rs(p));
	if(ls(p))tls(p).tag^=1;
	if(rs(p))trs(p).tag^=1;
	tree[p].tag=0;
}
void split(int p,int k,int &x,int &y)
{
	if(!p){x=y=0;return;}
	push_down(p);
	if(tls(p).size<k){x=copy_node(p);split(rs(x),k-tls(p).size-1,rs(x),y);push_up(x);}
	else{y=copy_node(p);split(ls(y),k,x,ls(y));push_up(y);}
}
int merge(int x,int y)
{
	if(!x||!y)return x|y;
	push_down(x);push_down(y);
	if(t(x).rand<t(y).rand){rs(x)=merge(rs(x),y);push_up(x);return x;}
	else{ls(y)=merge(x,ls(y));push_up(y);return y;}
}



本题中,我们一共要实现4个操作(单点插入,单点删除,区间反转,区间求和)。
暂且抛开可持久化不谈,具体实现起来也不难。
1.插入:
在第p个数后插入数x,就是把p拆下来然后再使用两遍merge,将它们粘在一起。

插入操作代码:

		if(op==1)
		{
			scanf("%lld%lld",&a,&b);
			a^=lastans;b^=lastans;
			split(rt[v],a,x,y);
			rt[++cnt]=merge(merge(x,new_node(b)),y);
		}

2.删除:
删掉第p个数,就是将它的两头分别拆下来,再拼接在一起。

删除操作代码:

        if(op==2)
        {
            scanf("%lld",&a);
            a^=lastans;
            split(rt[v],a,x,z);
            split(x,a-1,x,y);
            rt[++cnt]=merge(x,z);
        }

3.翻转:
将区间[l,r]翻转,就是将要反转的区间给拆下来,打上标记,再粘回去。

        if(op==3)
        {
            scanf("%lld%lld",&a,&b);
            a^=lastans;b^=lastans;
            split(rt[v],b,x,z);
            split(x,a-1,x,y);
            t(y).tag^=1;
            rt[++cnt]=merge(merge(x,y),z);
        }

4.查询:
查询区间[l,r]的最大值,就是将该区间拆下来,输出树根,再粘回去。

查询操作代码:

        if(op==4)
        {
            scanf("%lld%lld",&a,&b);
            a^=lastans;b^=lastans;
            split(rt[v],b,x,z);
            split(x,a-1,x,y);
            printf("%lld\n",lastans=t(y).sum);
            rt[++cnt]=merge(merge(x,y),z);
        }

代码贴一下:

		scanf("%d%d",&v,&op);
		if(op==1)
		{
			scanf("%lld%lld",&a,&b);
			a^=lastans;b^=lastans;
			split(rt[v],a,x,y);
			rt[++cnt]=merge(merge(x,new_node(b)),y);
		}
		if(op==2)
		{
			scanf("%lld",&a);
			a^=lastans;
			split(rt[v],a,x,z);
			split(x,a-1,x,y);
			rt[++cnt]=merge(x,z);
		}
		if(op==3)
		{
			scanf("%lld%lld",&a,&b);
			a^=lastans;b^=lastans;
			split(rt[v],b,x,z);
			split(x,a-1,x,y);
			t(y).tag^=1;
			rt[++cnt]=merge(merge(x,y),z);
		}
		if(op==4)
		{
			scanf("%lld%lld",&a,&b);
			a^=lastans;b^=lastans;
			split(rt[v],b,x,z);
			split(x,a-1,x,y);
			printf("%lld\n",lastans=t(y).sum);
			rt[++cnt]=merge(merge(x,y),z);
		}



为什么FHQ Treap可以依靠可持久化来优化空间复杂度呢?
其实很简单,就是因为Split过程中可以对点进行复制,并且每次修改的必然只有一个子树上的点。
而且Split和Merge总是成对出现,我们就只用复制一次。



完整代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
#define INF 0x3f3f3f3f
#define ls(p) tree[p].lson
#define rs(p) tree[p].rson
#define tls(p) tree[ls(p)]
#define trs(p) tree[rs(p)]
#define t(p) tree[p]
#define tpi t(++tot)
#define tp t(tot)
using namespace std;
const int N(2e5);
int n;ll lastans;
struct node
{
	int rand,size,tag;
	ll val,sum;
	int lson,rson;
}tree[(N<<7)+10];
int rt[N+10];
inline int new_node(long long v=0)
{
	static int tot(0);
	tpi.val=v;tp.sum=v;
	tp.rand=rand();tp.size=1;
	return tot;
}
inline int copy_node(int p)
{
	int ret=new_node();
	tree[ret]=tree[p];
	return ret;
}
inline void push_up(int p)
{
	tree[p].size=tls(p).size+trs(p).size+1;
	tree[p].sum=tls(p).sum+trs(p).sum+t(p).val;
}
inline void push_down(int p)
{
	if(!t(p).tag)return;
	if(ls(p))ls(p)=copy_node(ls(p));
	if(rs(p))rs(p)=copy_node(rs(p));
	swap(ls(p),rs(p));
	if(ls(p))tls(p).tag^=1;
	if(rs(p))trs(p).tag^=1;
	tree[p].tag=0;
}
void split(int p,int k,int &x,int &y)
{
	if(!p){x=y=0;return;}
	push_down(p);
	if(tls(p).size<k){x=copy_node(p);split(rs(x),k-tls(p).size-1,rs(x),y);push_up(x);}
	else{y=copy_node(p);split(ls(y),k,x,ls(y));push_up(y);}
}
int merge(int x,int y)
{
	if(!x||!y)return x|y;
	push_down(x);push_down(y);
	if(t(x).rand<t(y).rand){rs(x)=merge(rs(x),y);push_up(x);return x;}
	else{ls(y)=merge(x,ls(y));push_up(y);return y;}
}
int main()
{
	srand(224144);scanf("%d",&n);
	int cnt(0);int v,op;ll a,b;int x,y,z;
	while(n--)
	{
		scanf("%d%d",&v,&op);
		if(op==1)
		{
			scanf("%lld%lld",&a,&b);
			a^=lastans;b^=lastans;
			split(rt[v],a,x,y);
			rt[++cnt]=merge(merge(x,new_node(b)),y);
		}
		if(op==2)
		{
			scanf("%lld",&a);
			a^=lastans;
			split(rt[v],a,x,z);
			split(x,a-1,x,y);
			rt[++cnt]=merge(x,z);
		}
		if(op==3)
		{
			scanf("%lld%lld",&a,&b);
			a^=lastans;b^=lastans;
			split(rt[v],b,x,z);
			split(x,a-1,x,y);
			t(y).tag^=1;
			rt[++cnt]=merge(merge(x,y),z);
		}
		if(op==4)
		{
			scanf("%lld%lld",&a,&b);
			a^=lastans;b^=lastans;
			split(rt[v],b,x,z);
			split(x,a-1,x,y);
			printf("%lld\n",lastans=t(y).sum);
			rt[++cnt]=merge(merge(x,y),z);
		}
	}
	return 0;
}


其实这里就是本总结理论上结束的点,但是我还是整理了其他几个平衡树(虽然不太常用)




所谓的"替罪羊树",指的是重量平衡树
与一般的平衡树不同,替罪羊树是一颗"暴力"平衡树。
怎么讲呢?
替罪羊树并不依靠平衡化旋转来维持自身的平衡,而是当某颗子树的结构不满足所预设的平衡条件时,就立刻将整棵子树拆成区间再重建。

<插播>
其实替罪羊树这种重建方法一次只对一棵子树造成影响。
所以是可以可持久化的。

什么是替罪羊树的平衡条件呢?
这还真是个学问,因为替罪羊树重构子树p需要O(log(p.size))的时间复杂度(将一个点splay到根上去也才O(logn)),不能够太过频繁地进行重构,容易超时。
这就引出了"重量"平衡树的由来:

替罪羊树平衡条件:
设需判断的点为p,
规定当max(ls(p).size,rs(p).size)>p.size*α时
以p为根的子树是不平衡的。
其中α是一个满足α∈(0.5,1)的定值(常量)。

真是妙不可言。
下面来介绍一下替罪羊树的"拍扁重建"操作。
拍扁重建要3个函数:
将子树拍扁为有序序列的travel函数:

void travel(int p,vector<int>& x)
{
    if(!p)return;
    travel(ls(p),x);
    if(cnt(p))x.push_back(p);
    else bc[bc_top++]=p;
    travel(rs(p),x);
}

将有序序列建成平衡的BST(二分建树)的devide函数:

int divide(vector<int>& x,int l,int r) 
{
    if(l>=r)return 0;
    int mid=(l+r)>>1;
    int p=x[mid]; 
    ls(p)=divide(x,l,mid);
    rs(p)=divide(x,mid+1,r);
    push_up(p);
    return p;
}

拿来调用的rebuild函数:

void rebuild(int& p) 
{
    static vector<int> v;     
	v.clear();
    travel(p,v);
    p=divide(v,0,v.size());
}

图示:

例题:
Luogu P3369 【模板】普通平衡树
代码:

#include 
#include 
using std::vector;

namespace Scapegoat_Tree {
    const int maxn = 100000 + 10;
    const double alpha = 0.75;    //旋转因子 
    struct Node {
        Node* ch[2];              //左右子节点 
        int key,siz,cover;        //key是值,siz是以该节点为根的树的存在的节点数,cover是所有节点数量 
        bool exist;               //exist标志该节点是否被删除 
        void pushup() {           //更新函数 
            this->siz=ch[0]->siz+ch[1]->siz+(int)exist;
            this->cover=ch[0]->cover+ch[1]->cover+1; 
        }
        int isbad() {             //判断是否要重构 
            return (ch[0]->cover>this->cover*alpha+5)||(ch[1]->cover>this->cover*alpha+5);
        }
    };
    struct STree {
        protected:
            Node mempol[maxn];           //内存池 
            Node *tail,*null,*root;      //tail为指向内存池元素的指针 
            Node *bc[maxn];              //内存回收池(栈) 
            int bc_top;                  //内存回收池(栈)顶指针 
            
            Node* newnode(int key) {
                Node* p=bc_top?bc[--bc_top]:tail++;
                p->ch[0]=p->ch[1]=null;
                p->cover=p->siz=p->exist=1;
                p->key=key;
                return p;
            }
            
            void travel(Node* p,vector<Node*>& x) {    //将一棵树转化成序列,保存在vector中 
                if(p==null) return;                    //如果是空树则退出 
                travel(p->ch[0],x);                    //递归操作左子树 
                if(p->exist) x.push_back(p);           //如果该节点存在则放入序列中 
                else bc[bc_top++]=p;                   //回收内存,将不用的节点扔到内存回收池(栈)中 
                travel(p->ch[1],x);                    //递归操作右子树 
            }
            
            Node* divide(vector<Node*>& x,int l,int r) {    //返回建好的树 
                if(l>=r) return null;                       //序列为空不用建树 
                int mid=(l+r)>>1;
                Node* p=x[mid];                             //mid保证平衡 
                p->ch[0]=divide(x,l,mid);                   //递归操作 
                p->ch[1]=divide(x,mid+1,r);                 //递归操作 
                p->pushup();                                //维护节点信息 
                return p; 
            }
            
            void rebuild(Node*& p) {
                static vector<Node*> v;
                v.clear();
                travel(p,v);                //拍扁 
                p=divide(v,0,v.size());     //建树 
            }
            
            Node** insert(Node*& p,int val) {                     //返回指向距离根节点最近的一棵不平衡的子树的指针 
                if(p==null) {
                    p=newnode(val);
                    return &null;
                } else {
                    p->siz++,p->cover++;                          //维护节点数 
                    Node** res=insert(p->ch[val>=p->key],val);
                    if(p->isbad()) res=&p;
                    return res;
                }
            }
            
            void erase(Node*& p,int k) {
                p->siz--;                               //维护siz 
                int offset=p->ch[0]->siz+p->exist;      //计算左子树的存在的节点总数 
                if(p->exist&&k==offset) {               //判断当前节点权值是否第k小 
                    p->exist=false;                     //删除节点 
                } else {
                    if(k<=offset) erase(p->ch[0],k);    //如果k小于等于offset,递归操作左子树 
                    else erase(p->ch[1],k-offset);      //反之递归操作右子树 
                }
            }
            
            void iod(Node* p) {
                if(p!=null) {
                    iod(p->ch[0]);
                    printf("%d ",p->key);
                    iod(p->ch[1]);
                }
            }
        public:
            void init() {
                tail=mempol;                         //tail指向内存池的第一个元素 
                null=tail++;                         //为null指针分配内存 
                null->ch[0]=null->ch[1]=null;        //null的两个儿子也是null 
                null->cover=null->siz=null->key=0;   //null的所有标记都是0 
                root=null;                           //初始化根节点 
                bc_top=0;                            //清空栈 
            }
            
            STree() {
                init();
            }
            
            void insert(int val) {
                Node** res=insert(root,val);
                if(*res!=null) rebuild(*res);
            }
            
            int rank(int val) {
                Node* now=root;
                int ans=1;
                while(now!=null) {
                    if(now->key>=val) now=now->ch[0];
                    else {
                        ans+=now->ch[0]->siz+now->exist;
                        now=now->ch[1]; 
                    }
                }
                return ans;
            }
            
            int kth(int val) {
                Node* now=root;
                while(now!=null) {
                    if(now->ch[0]->siz+1==val&&now->exist) return now->key;
                    else if(now->ch[0]->siz>=val) now=now->ch[0];
                    else val-=now->ch[0]->siz+now->exist,now=now->ch[1];
                }
            }
            
            void erase(int k) {              //删除值为k的元素 
                erase(root,rank(k));
                if(root->siz<root->cover*alpha) rebuild(root);
            }
            
            void erase_kth(int k) {          //删除第k小 
                erase(root,k);
                if(root->siz<root->cover*alpha) rebuild(root);
            }
            
            void iod() {                     //调试用的中序遍历 
                Node* p=root;
                iod(p);
            }
    };
}

using namespace Scapegoat_Tree;
STree st; 
int main(int argc,char** argv) {
    int n,opt,que;
    scanf("%d",&n);
    while(n--) {
        scanf("%d%d",&opt,&que);
        if(opt==1) st.insert(que);
        if(opt==2) st.erase(que);
        if(opt==3) printf("%d\n",st.rank(que));
        if(opt==4) printf("%d\n",st.kth(que));
        if(opt==5) printf("%d\n",st.kth(st.rank(que)-1));
        if(opt==6) printf("%d\n",st.kth(st.rank(que+1)));
        if(opt==7) st.iod();
    }
    return 0;
}




你可能感兴趣的:(总结)