Splay 总结及模板


<目录>
0.NewNode 操作——————————–(新建一个节点)
1.Splay、Rotate 操作 ————————(Splay 旋转<核心>)
2.GetPre、GetNext 操作 ——————–(获得前驱后继)
3.GetMin、GetMax 操作 ——————– (获得最小值,最大值)
4.Insert 操作 ————————————(插入节点,以二叉排序树原则)
5.Delete 操作 ———————————– (删除节点)
6.Find_Kth 操作 ——————————— (获取第K大点)
7.Insert_Kth 操作 —————————— (将节点插入序列第K个位置)
8.PushUp操作 ——————————— (维护区间信息)
9.Merge 操作 ———————————— (将两个子树合并)
10.CoutAns 操作 ———————————-(输出整个序列)
11.GetSequence 操作 ———————— (获得一段序列)
12.Insert_Sequence 操作 ——————— (将序列插入现序列的K位置)
13.PushDown 操作 —————————–(维护’线段树’Lazy下放标记)
14.Delete_UpK 操作 —————————–(删除 大于或小于K的所有数)
15.Same 操作 ————————————–(当操作涉及重复元素)
16.极简代码
快速链接:


前言:

本文中,ll为define long long ,x为splay节点,tarfa为目标父亲位置,
ls为左儿子,rs为右儿子,fa为父亲,sz为子树大小,root为当前树的根。
splay的精华在于每次Insert等后都进行Splay旋转,这样就保证了树的高度永远为logN级别。


0.NewNode 操作(新建一个节点)

直接上代码:

IL void NewNode(RG int Value){       //新建一个权值为Value的结点
    fa[++cnt] = ls[cnt] = rs[cnt] = 0;
    blg[cnt] = cnt; val[cnt] = Value; sum[cnt] = 1; return;
}

此函数中的各个变量用处会在下文中有所展现。


1.Splay、Rotate 操作 (Splay 旋转<核心>)

没什么好说的。
Left_Rotate:

IL void Left_Rotate(RG ll x){
    RG ll y = fa[x],z = fa[y];
    fa[ls[x]] = y; rs[y] = ls[x];
    fa[x] = z; if(z){if(ls[z]==y)ls[z]=x; else rs[z]=x;}
    fa[y] = x; ls[x] = y; Update(y); Update(x);
}

Right_Rotate:

IL void Right_Rotate(RG ll x){
    RG ll y = fa[x],z = fa[y];
    fa[rs[x]] = y; ls[y] = rs[x];
    fa[x] = z; if(z){if(ls[z]==y)ls[z]=x; else rs[z]=x;}
    fa[y] = x; rs[x] = y; Update(y); Update(x);
}

Splay:

IL void Splay(RG ll x,RG ll tarfa){      //把x splay到tarfa的儿子
    while(fa[x] != tarfa){
        RG ll y = fa[x],z = fa[y];
        if(z == tarfa){
            if(ls[y] == x)Right_Rotate(x);
            else if(rs[y] == x)Left_Rotate(x);
        }
        else if(z != tarfa){
            if(ls[z] == y && rs[y] == x){Left_Rotate(x); Right_Rotate(x);}
            else if(rs[z] == y && ls[y] == x){Right_Rotate(x); Left_Rotate(x);}
            else if(ls[z] == y && ls[y] == x){Right_Rotate(x); Right_Rotate(x);}
            else if(rs[z] == y && rs[y] == x){Left_Rotate(x); Left_Rotate(x);}
        }
        if(!tarfa)root = x; 
    }return;
}

2.GetPre、GetNext 操作 (获得前驱后继)

以获取node节点的前驱,后继为例。

GetPre(获取前驱):

IL ll Get_pre(RG ll x){                    
    Splay(x,0); x = ls[x]; 
    while(rs[x])x = rs[x];  return x;
}
Pre_node = Get_pre(ls[node]);    //即左子树中最大的。

GetNext(获取后继):

IL ll Get_next(RG ll x){
    Splay(x,0); x = rs[x]; 
    while(ls[x])x = ls[x];  return x;
}
Next_node = Get_Next(rs[node]);   //即右子树中最小的。

3.GetMin、GetMax 操作 (获得最小值,最大值)

即序列的最左端点与最右端点,类似GetPre,GetNext。

GetMin:

IL void GetMin(RG ll x){while(ls[x])x = ls[x];  return x;}
Min = GetMin(root);

GetMax:

IL void GetMax(RG ll x){while(rs[x])x = rs[x];  return x;}
Max = GetMax(root);

4.Insert 操作 (插入节点,以二叉排序树原则)

与二叉排序树一样,以按照val从大到小为排序原则为例。

void Insert(RG ll rt,RG ll nw){
    if(val[rt]>val[nw] && !ls[rt]){ls[rt]=nw; fa[nw]=rt; return;}
    else if(val[rt]>val[nw] && ls[rt])Insert(ls[rt],nw);  
    else if(val[rt]<val[nw] && !rs[rt]){rs[rt]=nw; fa[nw]=rt; return;}
    else if(val[rt]<val[nw] && rs[rt])Insert(rs[rt],nw); 
}
//把x节点插入: Insert(root,x);

5.Delete 操作 (删除节点)

以删除x节点为例子。

IL void Delete(RG ll x){                                  
    Splay(x,0);
    if(!ls[x] && !rs[x]){root = 0; return;}
    if(!ls[x] && rs[x]){fa[rs[x]] = 0; root = rs[x]; return;}
    if(!rs[x] && ls[x]){fa[ls[x]] = 0; root = ls[x]; return;}
    RG ll Rc = GetMin(rs[x]);  Splay(Rc,0);
    ls[Rc] = ls[x]; fa[ls[x]] = Rc; fa[Rc] = 0;
    root = Rc;  Splay(Rc); return;
}

解释一下:
首先预先找到x节点。 然后Splay x 到根节点。
这时候分4中情况讨论

<1> (!ls[x] && !rs[x]):树中已经没有节点,root = 0;
<2> (!ls[x] && rs[x]): 让右儿子成为新的根即可。
<3> (!rs[x] && ls[x]):让左儿子成为新的根即可。
<4> (ls[x] && rs[x]):这种情况比较麻烦,见下面详解:
     {
          我们拟定把左子树接到右子树上面去。
          先找到右子树的Min,Rc = GetMin(rs[x]);
          然后把Rc Splay到根部去。
          那么此时Rc即为ls[x]的后继。
          此时让Rc成为新的根,root = Rc,fa[Rc] = 0;
          连接一下即可:ls[Rc] = ls[x],fa[ls[x]] = Rc;
     }

6.Find_Kth 操作 (获取第K大点)

也类似二叉排序树:

ll Find(RG ll rt,RG ll K){                  
    if(sz[ls[rt]] == K-1)return rt;
    else if(sz[ls[rt]] >= K && ls[rt])return Find(ls[rt],K);
    else return Find(rs[rt],K-sz[ls[rt]]-1);   //注意要减1(自己)
}
Kth = Find(root,K);

7.Insert_Kth 操作 (将节点插入序列第K个位置)

注意一下return判断的方式即可。 以把x节点插入到第K个位置为例。

void Ins_Kth(RG ll rt,RG ll nw,RG ll K){                           
    if(K == 1 && !ls[rt]){ls[rt]=nw; fa[nw]=rt; return;}
    if(K == 2+sz[ls[rt]] && !rs[rt]){rs[rt]=nw; fa[nw]=rt; return;}
    if(K<=1+sz[ls[rt]])Ins_Kth(ls[rt],nw,K);
    else Ins_Kth(rs[rt],nw,K-1-sz[ls[rt]]);    //注意减1(自己)
}

8.PushUp操作 (维护区间信息)

类似线段树的PushUp,以维护子树大小为例:

IL void PushUp(RG ll x){
    sz[x] = 1;
    if(ls[x])sz[x]+=sz[ls[x]];
    if(rs[x])sz[x]+=sz[rs[x]];  return;
}

这里归纳一下需要PushUp的地方(当然条件为相关函数存在)。

<1>.Rotate<以右旋为例>:

IL void Right_Rotate(RG ll x){                             
    RG ll y = fa[x],z = fa[y];
    ......
    PushUp(y); PushUp(x); return;    //请注意PushUp的顺序
}

<2>.Insert:

void Insert(RG ll rt,RG ll nw){
    //cout<<rt<<" "<<" "<<nw<<endl;
    if(val[rt]>val[nw] && !ls[rt]){... Update(rt); return;}
    else if(val[rt]>val[nw] && ls[rt]){...  Update(rt); return;}
    else if(val[rt]<val[nw] && !rs[rt]){... Update(rt); return;}
    else if(val[rt]<val[nw] && rs[rt]){...  Update(rt); return;}
}
//把x节点插入: Insert(root,x);

<3>Insert_Kth 、Insert_Sequence:与Insert类似
<4>Delete:

IL void Delete(RG ll x){                                  
    Splay(x,0);
    if(!ls[x] && !rs[x]){root = 0; return;}
    if(!ls[x] && rs[x]){fa[rs[x]] = 0; root = rs[x]; return;}
    if(!rs[x] && ls[x]){fa[ls[x]] = 0; root = ls[x]; return;}
    ......
    root = Rc; PushUp(Rc); Splay(Rc); return;
}

9.Merge 操作 (将两个子树合并)

用并查集维护是否需要合并,如果需要那么暴力启发式合并即可。
每个节点最多合并复杂度为logN<执行一次Insert操作>,所以总复杂度为N*logN是正确的。
以将 x1所在树 合并到 x2所在树为例。
其中bzj[i]为并查集数组,FindRoot为并查集操作。

void Merge_Visit(RG ll rt,RG ll x2){
    Insert(x2,rt);                    //将rt节点插入到 x2所在树中
    if(ls[rt])Merge_Visit(ls[rt],x2);
    if(rs[rt])Merge_Visit(rs[rt],x2);
}
void Merge_Start(RG ll x1,RG ll x2){
    Splay(x1,0);  Splay(x2,0);  
    if(sz[x1] > sz[x2])swap(x1,x2);   //x1 ----> x2,x1尽量小。             
    Merge_Visit(x1,x2); 
    return;
}

IL void Merge(){
    RG ll x = gi(),y = gi();
    RG ll f1 = FindRoot(x),f2 = FindRoot(y);
    if(f1 != f2){ Merge_Start(x,y);  bzj[f1]=f2; }
    return;
}

10.CoutAns 操作 (输出整个序列)

理解Splay原理后很容易知道,当前序列即为树的中序遍历。所以中序遍历一遍即可。

void CoutAns(RG ll rt){
    if(ls[rt])CoutAns(ls[rt]);  
    printf("%lld ",rt); 
    if(rs[rt])CoutAns(rs[rt]);
}
//CoutAns(root);

*

11.GetSequence 操作(获得一段序列操作)

以提取[ a , b ]为例。
一篇博客:http://blog.51cto.com/sbp810050504/1029553
注意一下上面那个博客中,把b+1提到a-1的儿子说的太复杂了,直接Splay(b+1,a-1)即可。
那么这里还是来概括一下具体步骤。
<1>Splay(a-1 , 0);
<2>Splay(b+1 , a-1 ) ;
<3>所求区间即为ls[ b+1 ]所在子树。
<4>可以以ls[b+1]为根,把一个区间当做一个点,进行移动,删除等操作。

这里有一个问题,a == 1 与 b == N 时会有鬼( T_T )。
处理方案有两个(这里Ans 表示提取的区间树的根节点):

【1】讨论思想(弱智思路无解释):

IL void GetSequence(){
    RG ll a = gi() , b = gi(); 
    RG ll L = Find(root,a) , R = Find(root,b);
    RG ll Min = GetMin(),Max = GetMax();
    RG ll Ro = Lo = GetPre(L),GetNext(R);
    if(L== Min && R == Max){Ans = root; return;}
    if(L == Min){Splay(Ro,0); Ans = ls[root]; return;}
    if(R == Max){Splay(Lo,0); Ans = rs[root]; return;}
    else {Splay(Lo,0); Splay(Ro,Lo); Ans = ls[Ro]; return;}
}

这里极其不推荐这种方法,这种方法会导致常数超级巨大,建议使用下面那种方法。

【2】虚点思想:
建立两个虚点,N+1表示最右端的那个点,N+2表示最左端那个点。
注意一下根据排序原则把val[N+1] = INF,val[N+2] = -INF确保其位置不变 。
对于PushUp等操作,把它当做正常点做就行了。
那么注意需要改变的是:
<1>Find(root,K) 应该为 Find(root,K+1)
<2>CoutAns(root)时,如果rt == (N+1 || N+2) 则不输出。

IL void GetSequence(){
    RG ll a = gi() , b = gi(); 
    RG ll L = Find(root,a) , R = Find(root,b+2);
    //  Find(root,a-1) ⇒ Find(root,a) ; Find(root,b+1) ⇒ Find(root,b+2)  ;
    Splay(L,0);Splay(R,L);
    Ans = ls[R]   return;
}

void CoutAns(RG ll rt){
    if(ls[rt])CoutAns(ls[rt]);  
    if(rt != N+1 && rt != N+2)printf("%lld ",rt); 
    if(rs[rt])CoutAns(rs[rt]); return;
}

int main(){
    ......
    val[N+1] = INF Insert(root,N+1);
    val[N+2] = -INF; Insert(root,N+2);
    ......
}

12.Insert_Sequence 操作 (将序列插入现序列的K位置)

以把序列插入原来序列的K位置为例。
通过GetSequence , 得到 待插入序列 的根Rot;
<1>Rot = GetMin(Rot);
<2>Insert_Kth( root , Rot , K );
代码略,其实就是两个步骤合起来。


13.PushDown 操作 (维护’线段树’Lazy下放标记)

与线段树类似,但是一定要注意该PushDown的位置一定要PushDown。
这里以维护区间翻转问题为例,dt[i]为lazy标记,主动修改lazy的 视题目而定函数 略。

【0】PushDown函数:

IL void PushDown(RG ll x){
    RG ll rson = rs[x],lson = ls[x];
    dt[rson] ^= 1; dt[lson] ^= 1; dt[x] = 0;
    ls[x] = rson; rs[x] = lson; return;
}

//
那么需要PushDown的部分有:

【1】Splay :

IL void Splay(RG ll x,RG ll tarfa){
    RG ll tt = x,cnt = 0;  
    while(tt != tarfa){tmp[++cnt] = tt; tt = fa[tt];} 
    while(cnt--)if(dt[tmp[cnt]])PushDown(tmp[cnt]);
    while(fa[x] != tarfa){
        ......
    }return;
}

具体来说,在splay的while之前,先:
<1>把splay向上翻转路径上的点都抠出来。
<2>对于这条路径,从上往下先把lazy标记都放了。
这样在Rotate时,就排除了lazy标记的干扰。

.
【2】Find:

ll Find(RG ll rt,RG ll K){
    if(dt[rt])PushDown(rt);                                
    ......
}

【3】CoutAns:

void CoutAns(RG ll rt){
    if(dt[rt])PushDown(rt);
    ......
}

【4】(!) GetMin、GetMax;
在跳转之前先要PushDown!! (最易错的地方); 以GetMax为例:

IL ll GetMax(){
    RG ll t = root; 
    while(1){
        if(dt[t])PushDown(t);  
        if(rs[t])t=rs[t]; else break;
    } return t;
}

注:、GetPre、GetNext操作由于需要进行Splay,所以不需要。

【5】Insert,Insert_Kth,Insert_Sequence:
以Insert函数为例:

void Insert(RG ll rt,RG ll nw){
   if(dt[rt])PushDown(rt);
   ...... 
}
//把x节点插入: Insert(root,x);

【6】视题目而定,反正能PushDown一下就PushDown一下。


14.Delete_UpK 操作 (删除大于或小于K的所有数)

以删除小于K的所有数为例:

IL void Delete_DnK(){
    RG ll Mix = K-1; if(!root)return;       //记得减1,确保 ==K 不会被剪出去
    fa[++cnt] = ls[cnt] = rs[cnt] = 0; val[cnt] = Mix;
    Insert(root,cnt); 
    pre = GetPre(cnt);
    if(!pre){root = 0; return;}
    Splay(pre); rs[pre] = 0;  
}

具体来说,步骤为:
<1> 向树中插入一个大小为K-1的节点
<2> 将这个节点Splay到根。
<3>找到这个点的前驱pre,Splay pre到根节点
<4>此时pre右侧的所有点即为小于等于K-1的点,直接剪掉右子树(包含新增点)即可。


15.Same 操作 (当操作涉及重复元素)

重复元素时,我们只需对于每一个点记录一个sum值,表示这个点的对应点数。
但是注意要标记每个点的映射关系,初始blg[i]=i,用处下文讲;
具体来说,
<1>Insert类操作:

void Insert(RG ll rt,RG ll nw){
    if(val[rt] == val[nw]){sum[rt]++; blg[nw] = rt;return;}
    .....
}

<2>Find类操作:

ll Find(RG ll rt,RG ll K){
    if(sz[ls[rt]]+1 <= K && K <= sz[ls[rt]]+sum[rt])return rt;
    if(sz[ls[rt]] >= K)return Find(ls[rt],K);
    else return Find(rs[rt],K-sum[rt]-sz[ls[rt]]);
}

然后是blg的作用:当你插入一个点后要操作时,请注意对应关系。
下面给一个例子:
Insert(root,x); Splay(x);
这个是会出问题的。因为如果有重复点,那么你只是把一个点Splay了。
正确做法是这样:
Insert(root,x); ll t = (blg[x]==x)?(x:blg[x]); Splay(t);


16.极简代码:
IL void Rot(int x){
    RG int y = fa[x],z = fa[y],c = Son(x);
    if(z)ch[z][Son(y)] = x; fa[x] = z;
    ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
    ch[x][!c] = y; fa[y] = x; PushUp(y);
}
IL void Splay(int x){
    //PushDown:
    RG int top = 0; S[++top] = x;
    for(RG int i = x; fa[i]; i = fa[i])S[++top] = fa[i];
    while(top)PushDown(S[top--]);
    //Splay:
    for(RG int y = fa[x]; fa[x]; Rot(x) , y = fa[x])
        if(z) Son(x) ^ Son(y) ? Rot(x) : Rot(y);
    PushUp(x);
}

嗯,就这么多吧。

一些练习的题目:
  1. P3224 [HNOI2012]永无乡 https://www.luogu.org/problemnew/show/P3224
  2. P3391 文艺平衡树(Splay)https://www.luogu.org/problemnew/show/P3391
  3. P1486 郁闷的出纳员 https://www.luogu.org/problemnew/show/P1486
  4. P2596 [ZJOI2006]书架 https://www.luogu.org/problemnew/show/P2596
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
东南亚位于我国倡导推进的“一带一路”海陆交汇地带,作为当今全球发展最为迅速的地区之一,近年来区域内生产总值实现了显著且稳定的增长。根据东盟主要经济体公布的最新数据,印度尼西亚2023年国内生产总值(GDP)增长5.05%;越南2023年经济增长5.05%;马来西亚2023年经济增速为3.7%;泰国2023年经济增长1.9%;新加坡2023年经济增长1.1%;柬埔寨2023年经济增速预计为5.6%。 东盟国家在“一带一路”沿线国家中的总体GDP经济规模、贸易总额与国外直接投资均为最大,因此有着举足轻重的地位和作用。当前,东盟与中国已互相成为双方最大的交易伙伴。中国-东盟贸易总额已从2013年的443亿元增长至 2023年合计超逾6.4万亿元,占中国外贸总值的15.4%。在过去20余年中,东盟国家不断在全球多变的格局里面临挑战并寻求机遇。2023东盟国家主要经济体受到国内消费、国外投资、货币政策、旅游业复苏、和大宗商品出口价企稳等方面的提振,经济显现出稳步增长态势和强韧性的潜能。 本调研报告旨在深度挖掘东南亚市场的增长潜力与发展机会,分析东南亚市场竞争态势、销售模式、客户偏好、整体市场营商环境,为国内企业出海开展业务提供客观参考意见。 本文核心内容: 市场空间:全球行业市场空间、东南亚市场发展空间。 竞争态势:全球份额,东南亚市场企业份额。 销售模式:东南亚市场销售模式、本地代理商 客户情况:东南亚本地客户及偏好分析 营商环境:东南亚营商环境分析 本文纳入的企业包括国外及印尼本土企业,以及相关上下游企业等,部分名单 QYResearch是全球知名的大型咨询公司,行业涵盖各高科技行业产业链细分市场,横跨如半导体产业链(半导体设备及零部件、半导体材料、集成电路、制造、封测、分立器件、传感器、光电器件)、光伏产业链(设备、硅料/硅片、电池片、组件、辅料支架、逆变器、电站终端)、新能源汽车产业链(动力电池及材料、电驱电控、汽车半导体/电子、整车、充电桩)、通信产业链(通信系统设备、终端设备、电子元器件、射频前端、光模块、4G/5G/6G、宽带、IoT、数字经济、AI)、先进材料产业链(金属材料、高分子材料、陶瓷材料、纳米材料等)、机械制造产业链(数控机床、工程机械、电气机械、3C自动化、工业机器人、激光、工控、无人机)、食品药品、医疗器械、农业等。邮箱:market@qyresearch.com

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值