「Note」数据结构方向 - 数据结构进阶
1. 平衡树(FHQ-Treap)
1.1. 介绍
功能强大的平衡树,以至于我根本没学 Treap 以及 Splay(LCT 里的另谈),缺点就大概是常数大。
FHQ-Treap 核心操作在于分裂与合并。
因为合并时按照随机权值决定父子关系,期望复杂度为 \(\log n\)。
1.1.1 分裂
按照权值 \(val\) 分裂,给定 \(val\),将整棵平衡树 \(T\) 分裂为 \(T_x,T_y\) 使得 \(\forall i\in T_x,\forall j\in T_y\),有 \(val_i\le val<val_j\)。
对于当前节点 \(rt\),若 \(val_rt\le val\),则左子树以及 \(rt\) 都应分到 \(T_x\) 中,故只需向右递归右子节点;反之类似地递归左子节点。
void split(int rt,int &x,int &y,int val)
{
if(!rt){x=y=0;return;}
if(t[rt].val<=val)split(t[rt].rs,t[x=rt].rs,y,val);
else split(t[rt].ls,x,t[y=rt].ls,val);
push_up(rt);
return;
}
其中 push_up()
函数是用来更新节点信息的,一定不要忘写。
1.1.2 合并
给定两棵树 \(T_x,T_y\),前提是 \(\forall i\in T_x,\forall j\in T_y\),有 \(val_i<val_j\)。
对于要合并的 \(x,y\),我们根据优先级决定父子关系,根据 \(val_x<val_y\) 调用 merge()
合并对应子节点。
int merge(int x,int y)
{
if(!x||!y)return x|y;
if(t[x].key<t[y].key){t[x].rs=merge(t[x].rs,y);push_up(x);return x;}
t[y].ls=merge(x,t[y].ls);
push_up(y);
return y;
}
1.2. 其他功能
1.2.1. 插入值
按照 \(val-1\) 分裂为两棵树,再新建节点合并即可。
void ins(int val)
{
int x=0,y=0;
split(root,x,y,val-1);
root=merge(merge(x,new_node(val)),y);
return;
}
其中 new_node()
函数用于新建节点,返回节点编号,\(\mathrm{root}\) 为整棵平衡树的根。
1.2.2. 删除值(只删一个)
void del(int val)
{
int x=0,y=0,z=0;
split(root,x,z,val),split(x,x,y,val-1);
root=merge(merge(x,y=merge(t[y].ls,t[y].rs)),z);
return;
}
1.2.3. 查询值的排名
按照 \(val-1\) 分裂为 \(T_x,T_y\),\(siz_x+1\) 即答案,记得合并回来。
int rk(int val)
{
int x=0,y=0,res=0;
split(root,x,y,val-1);
res=t[x].siz+1;
root=merge(x,y);
return res;
}
1.2.4. 查询第 \(k\) 大
类似线段树上二分,若 \(k\le siz_{ls_{rt}}\) 则向左子节点递归,若 \(k=siz_{ls_{rt}}+1\) 则返回 \(val_{rt}\),否则向右子节点递归。
int kth(int k)
{
int now=root;
while(true)
{
if(k<=t[t[now].ls].siz)now=t[now].ls;
else if(k==t[t[now].ls].siz+1)return t[now].val;
else k-=t[t[now].ls].siz+1,now=t[now].rs;
}
}
1.2.5. 查找严格小于 / 大于 \(val\) 的前驱 / 后继
类似二分(可能),与上一过程类似。也可以采用分裂后找值。
int pre(int val)
{
int now=root,res=0;
while(true)
{
if(!now)return res;
if(val<=t[now].val)now=t[now].ls;
else res=t[now].val,now=t[now].rs;
}
}
int suf(int val)
{
int now=root,res=0;
while(true)
{
if(!now)return res;
if(val>=t[now].val)now=t[now].rs;
else res=t[now].val,now=t[now].ls;
}
}
1.2.6. 区间翻转
此操作要求维护序列平衡树,考虑维护每个位置的值,此时平衡树并不需要满足二叉搜索树性质。
修改时将修改区间按照 \(siz\) 分离出来,打翻转标记即可。
1.3. 例题
\(\color{royalblue}{P3369}\)
$\text{Code}$:
#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
//IO
inline int rd()
{
int ret=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
//--------------------//
const int N=1e5+5;
int n;
//--------------------//
struct FHQ_Treap
{
struct FHQ_Treap_Node
{
int siz,val,key;
int ls,rs;
}t[N];
int tot,root;
int new_node(int val)
{
t[++tot].val=val;
t[tot].key=rand();
t[tot].siz=1;
return tot;
}
void push_up(int rt)
{
t[rt].siz=t[t[rt].ls].siz+t[t[rt].rs].siz+1;
return;
}
void split(int rt,int &x,int &y,int v)
{
if(!rt){x=y=0;return;}
if(t[rt].val<=v)split(t[rt].rs,t[x=rt].rs,y,v);
else split(t[rt].ls,x,t[y=rt].ls,v);
push_up(rt);
return;
}
int merge(int x,int y)
{
if(!x||!y)return x|y;
if(t[x].key<t[y].key){t[x].rs=merge(t[x].rs,y);push_up(x);return x;}
t[y].ls=merge(x,t[y].ls);
push_up(y);
return y;
}
void ins(int val)
{
int x=0,y=0;
split(root,x,y,val-1);
root=merge(merge(x,new_node(val)),y);
return;
}
void del(int val)
{
int x=0,y=0,z=0;
split(root,x,z,val),split(x,x,y,val-1);
root=merge(merge(x,y=merge(t[y].ls,t[y].rs)),z);
return;
}
int rk(int val)
{
int x=0,y=0,res=0;
split(root,x,y,val-1);
res=t[x].siz+1;
root=merge(x,y);
return res;
}
int kth(int k)
{
int now=root;
while(true)
{
if(k<=t[t[now].ls].siz)now=t[now].ls;
else if(k==t[t[now].ls].siz+1)return t[now].val;
else k-=t[t[now].ls].siz+1,now=t[now].rs;
}
}
int pre(int val)
{
int now=root,res=0;
while(true)
{
if(!now)return res;
if(val<=t[now].val)now=t[now].ls;
else res=t[now].val,now=t[now].rs;
}
}
int suf(int val)
{
int now=root,res=0;
while(true)
{
if(!now)return res;
if(val>=t[now].val)now=t[now].rs;
else res=t[now].val,now=t[now].ls;
}
}
}F;
//--------------------//
int main()
{
n=rd();
for(int op,x,i=1;i<=n;i++)
{
op=rd(),x=rd();
if(op==1)F.ins(x);
else if(op==2)F.del(x);
else if(op==3)printf("%d\n",F.rk(x));
else if(op==4)printf("%d\n",F.kth(x));
else if(op==5)printf("%d\n",F.pre(x));
else printf("%d\n",F.suf(x));
}
return 0;
}
\(\color{royalblue}{P3391}\)
板子。
$\text{Code}$:
#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
const int N=1e5+5;
struct FHQ_Treap
{
struct FHQ_Treap_Node
{
int val,siz,ls,rs,key;
bool lazy;
}t[N];
int root,tot;
int new_node(int val)
{
t[++tot]={val,1,0,0,rand(),false};
return tot;
}
void tag(int rt)
{
swap(t[rt].ls,t[rt].rs);
t[rt].lazy^=1;
return;
}
void push_up(int rt)
{
t[rt].siz=t[t[rt].ls].siz+t[t[rt].rs].siz+1;
return;
}
void push_down(int rt)
{
if(!t[rt].lazy)
return;
tag(t[rt].ls);
tag(t[rt].rs);
t[rt].lazy^=1;
return;
}
void split(int rt,int &x,int &y,int siz)
{
if(!rt){x=y=0;return;}
push_down(rt);
if(t[t[rt].ls].siz>=siz)split(t[rt].ls,x,t[y=rt].ls,siz);
else split(t[rt].rs,t[x=rt].rs,y,siz-t[t[rt].ls].siz-1);
push_up(rt);
}
int merge(int x,int y)
{
if(!x||!y)return x|y;
push_down(x),push_down(y);
if(t[x].key<t[y].key){t[x].rs=merge(t[x].rs,y);push_up(x);return x;}
t[y].ls=merge(x,t[y].ls);
push_up(y);
return y;
}
void change(int l,int r)
{
int x,y,z;
split(root,x,z,r);
split(x,x,y,l-1);
tag(y);
root=merge(x,merge(y,z));
return;
}
void Tprint(int rt)
{
if(!rt)
return;
push_down(rt);
Tprint(t[rt].ls);
printf("%d ",t[rt].val);
Tprint(t[rt].rs);
return;
}
}T;
int n,m;
//--------------------//
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
T.root=T.merge(T.root,T.new_node(i));
for(int l,r,i=1;i<=m;i++)
scanf("%d%d",&l,&r),T.change(l,r);
T.Tprint(T.root);
return 0;
}
2. 树套树
3.1. 介绍
树套树可以用来维护二维的信息。
顾名思义,树套树就是一个数据结构套上另一个数据结构,一般来讲内部维护的信息都需要可合并,通过外层数据结构合并内层信息来达到维护二维信息的目的。
内层数据结构需要满足动态开点,外层数据结构如果可行则采用树状数组维护以减小常数,一般情况下树状数组套线段树较为通用。
3.2. 常用技巧
3.2.1. 动态的区间内偏序问题
树状数组维护一维,线段树维护另一维,在符合第一维要求的树状数组上查询第二维符合要求的信息,修改是简单的。
3.2.2. 动态第 \(k\) 小
静态第 \(k\) 小采用可持久化线段树,每一个位置都继承前一位置的信息,然后前缀相减。
动态第 \(k\) 小只要在树状数组的维护方式上进行继承信息即可。
3. LCT
3.1. 介绍
3.1.1. 基本概念
LCT 全名 Link-Cut-Tree,动态树,是用来维护动态森林的数据结构。
它支持以下操作(需要保证任意操作时刻维护的都为森林):
- 连边。
- 断边。
- 换根。
- 提取路径信息。
LCT 的大体思路是将每棵树拆分为若干条链,并用平衡树维护链中节点,维护关键字为节点深度,每一条链的状态都是动态的。
由于 LCT 结构的特殊性,一般我们用 Splay 维护链,更加抽象的,我们实际上并不直接利用类似 \(\mathrm{build}\) 的东西将树划分成若干链,我们在需要的时候动态维护我们需要的链,这基于了 Splay 方便的各种操作。
在维护的时候,对于每一个节点,我们选出至多一个儿子作为实儿子,令节点到实儿子的边为实边,剩余边为虚边。实边组成的链成为实链,因为实链是我们所选择的,所以 LCT 的才是灵活多变的,请牢记我们的实链是不断变化去维护信息的。
3.1.2. 辅助树
我们用 Splay 以节点深度为关键字维护每条实链上的点,虚边连接了一棵棵 Splay,每个 Splay 的根节点都有一条虚边指向其维护原链顶的父节点。以这样的方式连接,一棵树上的多个 Splay 就组成了一棵辅助树,多棵辅助树就构成了我们的 LCT。
一颗辅助树有如下性质:
- 中序遍历每棵 Splay 得到的点序列,对应其维护链从上至下的一条路径。
- 辅助树节点与原树节点一一对应。
- 每棵 Splay 根节点父节点并不为空,其用虚边指向了此 Splay 维护原链的父节点。需要注意的,根节点并不是其父节点的子节点(认父不认子)。
由于辅助树的以上性质,我们任何操作都不需要维护原树,辅助树可以在任何情况下提取出一棵原树。
需要注意的:
- 原树的根不等于辅助树的根,原树的父节点指向不等于辅助树的父节点指向。
- 虚实链是可以在辅助树上进行变换,实现了动态的树链剖分,这也是前文说到的“实链是不断变化去维护信息的”。
至于为什么在操作过程中需要翻转 Splay,目的是保证复杂度,具体证明大概是论文级的,这里不会提及,只需要记住即可。
3.1.3 实现过程
以洛谷模板题为例,进行实现过程的讲解。
维护一个结构体表示一个节点,其中含有以下信息:
\(fa\) | \(son_{1/0}\) | \(val\) | \(sum\) | \(siz\) | \(lazy\) |
---|---|---|---|---|---|
父节点 | 子节点 | 此节点权值 | 此节点在 Splay 中的子树异或和 | 此节点在 Splay 中的子树大小 | 翻转标记 |
介绍 LCT 的核心函数之前先给出两个 Splay 的函数(\(t\) 是我们的节点结构体数组)。
void rotate(int rt)
{
int fa=t[rt].fa,gfa=t[fa].fa,gs=get(rt),gfs=get(fa);
bool flag=che_rt(fa);
t[fa].son[gs]=t[rt].son[!gs],t[t[rt].son[!gs]].fa=fa;
t[rt].son[!gs]=fa,t[fa].fa=rt,t[rt].fa=gfa;
if(!flag)
t[gfa].son[gfs]=rt;
push_up(fa),push_up(rt);
return;
}
void splay(int rt)
{
push_down_(rt);
for(int now=t[rt].fa;!che_rt(rt);rotate(rt))
if(!che_rt(now=t[rt].fa))
rotate((get(now)==get(rt))?now:rt);
return;
}
rotate()
函数进行单旋操作,splay()
函数将节点旋至其所在 Splay 的根节点。与正常 Splay 不同的是,我们需要判断一个节点是否为其所在 Splay 的根(che_rt()
函数的作用),同时也请注意翻转标记的下传。
接下来我们介绍 LCT 的核心操作。
void access(int rt)
{
for(int lst=0;rt;lst=rt,rt=t[rt].fa)
{
splay(rt);
t[rt].son[1]=lst;
push_up(rt);
}
return;
}
此函数作用是把某节点到当前辅助树的根路径上的点合并为一棵新的 Splay,具体过程如下:
- 现将当前节点旋至其所在 Splay 的根。
- 将上一节点接到当前节点上(这样直接实现了新链的合并与原来 Splay 边的断开)。
- 更新当前节点信息。
- 跳转到父亲,重复操作直到跳至根。
我们的剩余函数都以 access()
函数为基础进行操作,接下来介绍其他的函数。
tag()
用于对节点打上旋转标记。
void tag(int rt)
{
swap(t[rt].son[0],t[rt].son[1]);
t[rt].lazy^=1;
return;
}
che_rt()
用于检查节点是否为 Splay 的根。
bool che_rt(int rt)
{
return (t[t[rt].fa].son[0]!=rt&&t[t[rt].fa].son[1]!=rt);
}
get()
用于找到当前节点是其父节点的哪个儿子。
int get(int rt)
{
return ((t[t[rt].fa].son[1]==rt)?1:0);
}
push_up()
更新当前节点信息。
void push_up(int rt)
{
t[rt].siz=t[t[rt].son[0]].siz+t[t[rt].son[1]].siz+1;
t[rt].sum=t[t[rt].son[0]].sum^t[t[rt].son[1]].sum^t[rt].val;
return;
}
push_down()
下传标记。
void push_down(int rt)
{
if(t[rt].lazy!=0)
{
tag(t[rt].son[0]);
tag(t[rt].son[1]);
t[rt].lazy=0;
}
return;
}
push_down_()
下传当前节点至 Splay 根节点的标记(从上至下)。
void push_down_(int rt)
{
if(!che_rt(rt))
push_down_(t[rt].fa);
push_down(rt);
}
make_rt()
使当前节点变为辅助树的根。
void make_rt(int rt)
{
access(rt);
splay(rt);
tag(rt);
return;
}
find_rt()
查询当前节点所在辅助树的根(请考虑splay()
的过程,不难得出结论)。
int find_rt(int rt)
{
access(rt);
splay(rt);
while(t[rt].son[0])
{
push_down(rt);
rt=t[rt].son[0];
}
splay(rt);
return rt;
}
check()
查询两节点是否连通。
bool check(int x,int y)
{
make_rt(x);
return (find_rt(y)==x);
}
link()
连边(先判连通)。
void link(int x,int y)
{
if(!check(x,y))
t[x].fa=y;
}
cut()
断边(判相连,仍然考虑实现过程即可理解)。
void cut(int x,int y)
{
if(check(x,y)&&t[x].siz<=2)
t[y].fa=t[x].son[1]=0;
}
split()
分离一条链,正是前文提到的“提取路径信息”。
int split(int x,int y)
{
make_rt(x);
access(y);
splay(y);
return y;
}
change()
单点修改(并不是所有的题都有)。
void change(int rt,int val)
{
splay(rt);
t[rt].val=val;
push_up(rt);
return;
}
对于模板题的构建至此结束,LCT 的拓展性较强,具体题目还需具体分析。
3.2. 常用技巧
3.2.1 基础技巧
对于维护操作应多考虑优化,可以复杂化 push_up()
函数,但要注意代码结构的简洁性,并尽可能减少代码量与数据结构的操作难度,即使可能使复杂度变为稍劣(在无风险前提下)。
3.2.?咕咕咕
咕咕咕
3.3. 题目
\(\color{blueviolet}{P3690}\)
板子。
$\text{Code}$:
#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
const int N=1e5+5;
struct LCT
{
struct Tree_Node
{
int fa,son[2];
int val,sum,siz;
int lazy;
}t[N];
void tag(int rt)
{
swap(t[rt].son[0],t[rt].son[1]);
t[rt].lazy^=1;
return;
}
bool che_rt(int rt)
{
return (t[t[rt].fa].son[0]!=rt&&t[t[rt].fa].son[1]!=rt);
}
int get(int rt)
{
return ((t[t[rt].fa].son[1]==rt)?1:0);
}
void push_up(int rt)
{
t[rt].siz=t[t[rt].son[0]].siz+t[t[rt].son[1]].siz+1;
t[rt].sum=t[t[rt].son[0]].sum^t[t[rt].son[1]].sum^t[rt].val;
return;
}
void push_down(int rt)
{
if(t[rt].lazy!=0)
{
tag(t[rt].son[0]);
tag(t[rt].son[1]);
t[rt].lazy=0;
}
return;
}
void push_down_(int rt)
{
if(!che_rt(rt))
push_down_(t[rt].fa);
push_down(rt);
}
void rotate(int rt)
{
int fa=t[rt].fa,gfa=t[fa].fa;
int gs=get(rt),gfs=get(fa);
bool flag=che_rt(fa);
t[fa].son[gs]=t[rt].son[!gs];
t[t[rt].son[!gs]].fa=fa;
t[rt].son[!gs]=fa;
t[fa].fa=rt;
t[rt].fa=gfa;
if(!flag)
t[gfa].son[gfs]=rt;
push_up(fa),push_up(rt);
return;
}
void splay(int rt)
{
push_down_(rt);
for(int now=t[rt].fa;!che_rt(rt);rotate(rt))
if(!che_rt(now=t[rt].fa))
rotate((get(now)==get(rt))?now:rt);
return;
}
void access(int rt)
{
for(int lst=0;rt;lst=rt,rt=t[rt].fa)
{
splay(rt);
t[rt].son[1]=lst;
push_up(rt);
}
return;
}
void make_rt(int rt)
{
access(rt);
splay(rt);
tag(rt);
return;
}
int find_rt(int rt)
{
access(rt);
splay(rt);
while(t[rt].son[0])
{
push_down(rt);
rt=t[rt].son[0];
}
splay(rt);
return rt;
}
bool check(int x,int y)
{
make_rt(x);
return (find_rt(y)==x);
}
void link(int x,int y)
{
if(!check(x,y))
t[x].fa=y;
}
void cut(int x,int y)
{
if(check(x,y)&&t[x].siz<=2)
t[y].fa=t[x].son[1]=0;
}
int split(int x,int y)
{
make_rt(x);
access(y);
splay(y);
return y;
}
void change(int rt,int val)
{
splay(rt);
t[rt].val=val;
push_up(rt);
return;
}
}L;
//--------------------//
int n,m;
//--------------------//
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&L.t[i].val);//L.t[i].sum=L.t[i].val;
for(int op,x,y,i=1;i<=m;i++)
{
scanf("%d%d%d",&op,&x,&y);
if(op==0)
printf("%d\n",L.t[L.split(x,y)].sum);
else if(op==1)
L.link(x,y);
else if(op==2)
L.cut(x,y);
else
L.change(x,y);
}
return 0;
}