• 7月清北学堂培训 Day 3


    今天是丁明朔老师的讲授~

    数据结构

    绪论

    下面是天天见的:

    栈,队列;

    堆;

    并查集;

    树状数组;

    线段树;

    平衡树;

    下面是不常见的:

    主席树;

    树链剖分;

    树套树;

    下面是清北学堂课程表里的:

    ST表;

    LCA;

    HASH;

    支持两种操作:

    1.插入一个值;

    2.删除一个最大值(大根堆)或最小值(小根堆);

    需要使用STL里的 priority_queue 或手写;

    LCA

    结点 A 和结点 B 的最近公共祖先 LCA 及以上都是 A 和 B 的公共祖先;

    注意 LCA 是尽局限于树上的;

    如何求两个结点 A 和 B 的 LCA?

    1. 如果 A 的深度比 B 的深度小,那么我们将 A 和 B 互换一下,这是为了方便处理;

    2. 把 A 向上抬升到 B 的深度;

    3. A 和 B 一块往上走,直到走到一个点为止;

    如何快速地将 A 和 B 抬升到一个深度?

    我们发现 A 和 B 是有深度差的,记为:deep = dA - dB ;

    如果我们一步一步地往上跳,要跳 deep 次,我们发现当这个树是一条链的话,时间复杂度会达到O(n),有很大的劣势,我们需要改进一下:

    我们可以设计这样一个数组:p [ x ][ i ] 表示 x 的第 2i 个祖先是哪个;

    边界条件:p [ x ][ 0 ] = y,y 是 x 的父亲,这个我们用深度优先搜索就可以实现;

    一个显然的递推方程:p [ x ][ i ] = p [ p [ x ][ i-1] ][ i-1 ];(x 向上走 2i 就相当于先走 2i-1 再走 2i-1

    我们可以将 deep 用二进制表示出来,为了便于理解这里设 deep=19 吧:

    deep = 19 = (10011)2 = 2+ 21 + 20 

    那么也就是说,我们可以将 A 先往上跳 24 ,再往上跳 21,再往上跳 20 ,也跳到了 B 的深度;

    那么我们看到我们定义的数组,不就是 A = p [ A ][ 4 ]  => A = p [ A ][ 1 ]  => A = p [ A ][ 0 ];我们只跳了三步就OK了。

    时间复杂度 O(log n);

    如何快速地将 A 和 B 走到同一位置?

    我们发现 A 和 B 一旦走到了最近公共祖先 LCA 后,那么以后肯定都在一个位置了,但是我们不好确定这个 LCA 在哪里;

    虽然不好确定 LCA 在哪,但是我们可以确定最后一次不相遇的位置:

    我们从大到小枚举 i ,让 A 和 B 同时跳 2i ,如果发现跳了之后还是到不了同一个点,那就跳,否则就不跳;

    证明的话很简单,因为 dLCA - dA 也可以用二进制表示出来,所以我们是一定能够到达这个 LCA 的,我们按照上述操作后,那么 A 和 B 一定就是 LCA 的左右两个儿子,所以我们再跳一次就是 LCA了;过程中主要如果能跳到同一点就不跳,因为我们不能确定这是不是 LCA;

    LCA 常运用处理一类带差分,可差分的问题:

    假如我们有棵树:

    我们要求结点 6 和 7 的最短路径,我们可以先求出 6 和 7 的LCA是 2,然后答案就是deep7 + deep- 2 * deep2:

    ST表

    主要是处理区间最值的 RMQ 问题;

    我们设 mx [ i ][ j ] 表示下表从 i ~ i + 2j - 1 内的最值是多少;

    边界条件:mx [ i ][ 0 ] = i;

    递推方程:mx [ i ][ j ] = max(mx [ i ][ j-1 ] , mx [ i+2j-1 ][ j-1 ]); 

                      mx [ i ][ j ] = min (mx [ i ][ j-1 ] , mx [ i+2j-1 ][ j-1 ]); 

    这里我们可以将 [ i , i+2j -1 ] 这个长度为 2j 的区间平均分成两个长度为 2j-1 的小区间:[ i , i+2j-1-1 ] 和 [ i+2j-1 , i+2j-1 ],那么大区间的答案不就是两个小区间的答案取最优嘛?这不就完了?

        for(int j=1;(1<<j)<=n;j++)
           for(int i=1;i+(1<<j)-1<=n;i++)
              f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);

    在考虑询问的时候,我们要找两段长度相同的区间能覆盖询问区间,我们可以这样做:

    先算出询问区间的长度 len = r - l + 1,然后我们取 len 的 log 值向下取整: t = floor(loglen),那么 2t 就能覆盖询问区间的一半,那么我们再来一个就能全部覆盖了。

            int l=read();
            int r=read();
            int k=(int)(log((double)(r-l+1))/log(2.0)); 
            int ans=max(f[l][k],f[r-(1<<k)+1][k]);

    哈希HASH

    HASH是一种函数,我们需要设计一种函数将一个字符串变成一个数,所以我们在比较两个字符串的时候,就可以比较两个数了;

    map 是基于比较函数的红黑树,两个字符串的比较是O(字符串长度),非常非常慢!

    我们怎么将一个字符串转化成HASH值?

    1.我们先设定这个字符串是个几进制的数(最好取质数);

    2.我们可以将原字符串里的字母转化成ASCII 码,然后再将其转化成十进制的数,就是这个字符串的HASH值了。注意到这个数可能很大,所以我们要在后面模一个大质数。考虑到unsigned long long 的范围是 1~ 264- 1,是个质数耶,所以我们可以用unsigned long long 来存让它自然溢出就行了,完全不用管取模的事。

    HASH是允许冲突的!我们只是要尽可能避免冲突!而不是根本上消除冲突! 如果我们非常害怕冲突,我们可以双哈希。(将这个字符串用两种进制表示,再模两个不同的质数)

    假设我们有一个字符串 dmstql,我们要将它转成HASH值:
    1.我们先设定这个字符串是个 p 进制的数;
    2.将其转化为十进制(字母换成ASCII码):
    HASH = d * p5 + m * p4 + s * p3 + t * p2 + q * p1 + l * p0

    我们怎么求一个字符串子串的HASH?我们求每个字符前缀的HASH,然后可以利用前缀和的思路来求子串的HASH:

    d:d * p0

    dm:d * p1 + m * p0

    dms:d * p2 + m * p1 + s * p0

    dmst:d * p3 + m * p2 + s * p1 + t * p0

    dmstq:d * p4 + m * p3 + s * p2 + t * p1 + q * p0

    dmstql:d * p5 + m * p4 + s * p3 + t * p2 + q * p1 + l * p0

    我们发现第 i 个字符前缀哈希值 = 第 i-1 个字符的前缀哈希值 * p + Si(Si 是第 i 个字符) 

    那么我们怎么求 stq 的哈希值?

    手写一下很显然,就是:s * p2 + t * p1 + q * p0,那么怎么用前缀和的形式来表达呢?

    其实很很显然了:

    HASHdmstq - HASHdm * p3

    = (d * p4 + m * p3 + s * p2 + t * p1 + q * p0)- (d * p1 + m * p0)* p3

    = (d * p4 + m * p3 + s * p2 + t * p1 + q * p0)- (d * p4 + m * p3

    = s * p2 + t * p1 + q * p0

    =HASHstq

    至于后面要乘上 p 的几次方这个问题,我们只要看我们求的这个字符串的长度就好了,这里 stq 的长度是 3,所以后面乘上 p3

    并查集

    支持合并集合和查找在哪个集合里

    定义一个数组:fa [ i ] 表示 i 的父亲是哪个结点,注意树根的父亲是自己;

    初始化:fa [ i ] = i,表示每个结点都是独立的;

    路径压缩:

    我们发现并查集完全没有必要保留树的结构,所以我们直接将一个结点 x 的父亲设为它的祖先;

    int getfa(int x)             //寻找x的父亲 
    {
        fa[x]==x?return x:return getfa(fa[x]);
    } 

    树状数组

    支持单点修改,区间查询;

    主要应用:

    线段树常数过大时

    线段树功能过多时

    树状数组所求的所有问题必须存在逆元!

    int lowbit(int x)        //求lowbit 
    {
        return x&(-x);
    }
    
    void modify(int x,int y) //将第x个数加上y 
    {
        for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;  //加lowbit找父亲 
    }
    
    int query(int x)         //询问x的前缀和 
    {
        int ret=0;
        for(int i=x;i;i-=lowbit(i)) ret+=c[i];
        return ret;
    }
    
    int query(int l,int r)   //区间[l,r]的和 
    {
        return query(r)-query(l-1);
    }

    二维树状数组

    树状数组的每一个节点都是一个树状数组,所以把循环复制一遍即可。

    线段树

    支持区间修改,区间查询;

    主要应用:

    用于处理一类区间修改区间查询的问题。

    树的每个结点是一个抽象的线段;

    单点修改:

    1.定位点的位置;

    2.更新树的权值;

    任何一段线段在线段树中都以用 log n 条线段表示;

    区间修改,区间查询:

    要用到懒标记 Lazy Tag,表示这个结点对应的区间的每个数都加上了 x(x存在 Lazy Tag 里);

    它的作用是:我们区间加上 x 后,我非常懒不立刻加,不询问到这个结点的话,我就啥也不干,询问到才加上这个 x;

    什么时候下传标记?

    只要我们要遍历到该结点,就要将它父亲的标记下放;

    struct Node{
        int l,r;
        int sum;
        int tag;
    }t[N<<2];
    
    void pushup(int rt){                     //num上传 
        t[rt].sum=t[rt<<1].sum+t[rt<<1|1].sum;
    }
    
    void pushdown(int rt){                   //标记下传 
        if(t[rt].tag){
            t[rt<<1].tag+=t[rt].tag;
            t[rt<<1].sum+=t[rt].tag*(t[rt<<1].r-t[rt<<1].l+1);
            t[rt<<1|1].tag+=t[rt].tag;
            t[rt<<1|1].sum+=t[rt].tag*(t[rt<<1|1].r-t[rt<<1|1].l+1);
            t[rt].tag=0;
        }
    }
     
    void build(int rt,int l,int r){           //建树 
        t[rt].l=l;
        t[rt].r=r;
        if(l==r){
            t[rt].sum=a[l];
            return;
        }
        int mid=(l+r)>>1;
        build(rt<<1,l,mid);
        build(rt<<1|1,mid+1,r);
        pushup(rt);
    }
    
    void modify(int rt,int p,int c){          //单点修改 
        if(t[rt].l==t[rt].r){
            t[rt].sum=c;
            return;
        }
        pushdown(rt);
        int mid=(t[rt].l+t[rt].r)>>1;
        if(p<=mid) modify(rt<<1,p,c);
        else modify(rt<<1|1,p,c);
        pushup(rt);
    }
    
    int query(int rt,int l,int r){            //询问区间[l,r]的和 
        if(l<=t[rt].l&&t[rt].r<=r){
            return t[rt].sum;
        }
        pushdown(rt);
        int ret=0;
        int mid=(t[rt].l+t[rt].r)>>1;
        if(l<=mid) ret+=query(rt<<1,l,r);
        if(mid<r) ret+=query(rt<<1|1,l,r);
        return ret;
    }
    
    void add(int rt,int l,int r,int c){       //[l,r]上每个数加上c 
        if(l<=t[rt].l&&t[rt].r<=r){
            t[rt].tag+=c;
            t[rt].sum+=c*(t[rt].r-t[rt].l+1);
            return;
        }
        pushdown(rt);
        int mid=(t[rt].l+t[rt].r)>>1;
        if(l<=mid) add(rt<<1,l,r,c);
        if(mid<r) add(rt<<1|1,l,r,c);
        pushup(rt);
    }

    总结:

    堆:最大值插入,删除,查询;

    ST表:区间最大值查询;

    树状数组:单点修改,区间查询;

    线段树:区间修改,区间查询;

    看例题:

    例一

    我们维护两个堆,一个大根堆,一个小根堆,使得大根堆内的元素个数是 n/2 + 1,小根堆内的元素个数是 n/2,每次插入的时候往大根堆里面插,如果元素个数超过了 n/2 + 1 的话我们就将大根堆的堆顶弹入小根堆里,插完之后大根堆的堆顶就是中位数。(这个的话应该挺好理解的:因为大根堆里面有 n/2 + 1个数,所以比堆顶元素小的有 n/2 个数,比堆顶元素大的都弹到小根堆里面了,也有 n/2 个数,那么这个数不就是中位数嘛?)

    例二

    显然我们每次合并两堆重量最小的果子一定是最优的,那么一个很简单很暴力的思路就是每次合并前从小到大排个序,然后合并最小两堆就好了,但是明显时间复杂度要炸,那么我们考虑用数据结构: 
    维护一个小根堆,每次合并取两次堆顶,合并之后再插入小根堆并维护形态,直到小根堆内的元素个数为1 。
     
    例三

    我们可以将每个点向右向下连一条边,权值就是这两个点的高度差的绝对值,然后我们将所有的边升序排序,每次取出一条边就将连着的两个端点合并,若发现集合中的点的个数等于T,那么这个集合的贡献就是:最新加入的这条边的权值 * 这个集合中出发点的个数;

    做法就是并查集啦~ 

    例四

    这个题是树的哈希。

    我们看到这个题没有规定树的根,这求起来就有点麻烦啊。不过我们看到数据范围很小,所以我们可以以每个结点为根求一个HASH,如果发现有两个HASH值完全相同,那么就说明这两棵树是同构的。

    更巧妙的做法:

    一个无根树的中心不会超过两个。

    枚举每个重心,以重心为根求出这棵有根树的最小表示,然后取字典序最大的即可。

    也可以用括号序来做:

    对于有根树的最小表示,可以看成括号序列,每次把子树的括号序列按字典序排序后依次串连起来即可。 

    父亲结点的括号括着儿子结点的括号,兄弟结点的括号是并列关系的。

     

    例五: 

    我们不用归并排序,考虑用树状数组做。

    假设我们有个序列 :

    1 9 2 6 0 8 1 7

    我们只要求出来每个数前面有几个数比它大,就是这个数贡献的逆序对数,我们只要把所有数的逆序对数加起来就好了。

    做法:

    我们开一个 vis 数组,每输入一个数,将它的 vis 值赋成 1, 

    问题变成了动态将某个点加一,动态维护前缀和;

    离散化:

    1.排序 sort;

    2.去重 unique;

    3.安排查找 lower_bound;

    例六: 

    由于我们要统计一个星星 i 左下角的星星数,那么就是要统计所有的 xj <= xi,yj <= yi,因为我们是按照 y 递增来输入的星星,所以所有比当前星星的 y 值小的星星都已经被输入了,那么我们就考虑之前输入的星星有多少颗星星的 x 值小于等于当前星星的 x 值就好了。

    我们开一个数组,S [ i ] 表示横坐标x为 i 的星星个数,那么所有横坐标小于等于 i 的星星个数就是:S [ 1 ] + S [ 2 ] + S [ 3 ] + ……+ S [ i ],求前缀和我们可以用树状数组! 

    这个题是二维偏序,一维排序,一维树状数组。

    例七: 

    我们开 m 个树状数组。

    第 i 个树状数组的第 j 个下标表示 aj % m 是否为 i,是则为1,否则为 0;

    加法减法还是正常的加加减减,我们重点考虑询问的情况:

    我们询问区间 [ l , r ] 有多少个数模 m ==mod,我们就在第 mod 个树状数组里面找,考虑到一段区间内的和就是这一段区间内模 n == i 的数的个数,所以我们可以利用前缀和思想(树状数组来维护前缀和)分别求出 sum [ r ] 和 sum [ l-1 ] 再做差就可以了。

    例八: 

    对于一个数 x,我们从前找不大于 x 的最大数和从后找不小于 x 的最小数,然后分别与 x 做差取最小的绝对值就是答案;

    我们建立一棵线段树维护区间最小值和最大值;(权值线段树:下标不是数组的下标,而是权值的下标)

    我们维护两个 Tag,一个记录加法,一个记录乘法,它们之间会互相影响;

    考虑到我们在区间乘法的时候,不仅乘法标记要乘上 x,加法标记也要乘上 x;标记下传的时候,考虑到乘法标记优先下传更优,所以将加法标记下传的时候也要乘下乘法标记;

    我们发现原数组 a 没有什么卵用啊,我们要求的是斐波那契数求和,所以我们用线段树来维护区间内的斐波那契数的和;对于我们将原数组的某个数加上了 x,其实就是该项的斐波那契数往后推了 x 项,那么我们直接在线段树中将该位置乘上((1 0)(1 1))x 就行了。

    这个题告诉我们,线段树懒标记打的不一定是个数,还可能是个矩阵或一些更加奇怪的东西。

    发现这个题跟昨天 lyd 讲的分块的题有些类似。

    我们开根号的时候,我们看看这个数是否已经被开到了1 或 0,如果是就打上个标记,以后再也不管了(√1=1,√0=0),如果一个结点的左右儿子都被打上标记了,那么我们就将这个结点打上标记;然后就做完了。

    满足插入一个数,删除一个数,求中位数之和。

    注意到我们插入删除数的时候,中位数可能会改变。

    我们开一个 s 数组,s [ i ] 表示下标模5为 i 的数的和;

    然后我们就可以线段树每一个结点维护这么一个数组:

    假如我们有一个序列:0 1 2 6 7 8 9 11 

    根结点只有一个元素,所以下标都是 1:

    然后得到倒数第二层的数,要将右儿子滚动左儿子数的个数次:

    例如:10000 -> 01000

    答案就是根结点的 s [ 3 ];

    具体做法:

    先将要处理的数字离散化。

    按数字的顺序为下标建立一颗线段树。

    线段树的每个节点维护如下几个值:

    这一段闭区间中有几个数字;

    s [ 0~4 ]表示下标模5余某的数值之和。

    单点修改,区间查询即可完成操作。

    告诉我们线段树里维护的不一定是个数,也可能是某种信息,这也是比较常考的。

    mex:没出现过的最小的自然数。

    我们从左往右扫一遍就可以得到所有以 1 为左端点的区间的 mex 值;

    我们每次讲左端点 l 右移一个单位,r 也不断改变,同时更新新区间的 mex 值;

    考虑到如果一个数在序列里仅出现过一次,那么如果将这个点删去的话,在这个点右边的那些 mex 比这个数大区间的 mex 值就会被更新成这个数。

    单词询问的时间复杂度是O(log n),它的复杂度就是区间修改;

    只有查询没有修改:

    1.线段树离线;

    2.莫队算法;

    其实这个题我们只要看有没有长度为 3 的等差序列就好啦。

    这个题一个灰常重要的前提:1~n 在序列里全都出现过一次!

    我们用一个 vis 数组,将之前出现过的数标记为1,没出现的数标记为0,不妨枚举等差中项 x,我们以 x 为对称轴,看看左右的 vis 是否对称,如果不对称就说明有解,否则的话就说明以 x 作为等差中项是无解的。

    单点修改,如何比较两段区间是否相同。

    线段树的每一个结点代表的维护这个结点的线段的哈希值,我们要维护两种哈希值,一个往前一个往后。

    举个例子:

    我们有个序列: 9 3 1 7 5 6 8 2

    我们先插入9,将 vis [ 9 ] 标记为 1,并看看以 9 为对称轴两边的 vis 值是否对称:

    再插入3,将 vis [ 3 ] 标记为 1,并判断以 3 为对称轴左右的 vis 值是否对称:

    再插入1:

    再插入7:

     为什么可以介个样做呢?

    考虑到当前插入一个数是 x 吧,发现 vis [ x - a ] = 1,说明 x - a 在 x 之前已经出现过了,则 x - a 在 x 的左边;我们又发现 vis [ x + a ] = 0 (x + a <=n),说明 x + a 会在 x 之后出现,这样不就有了一个长度为 3 的等差序列了嘛?这道题就做完了。

    平衡树

    二叉搜索树的性质:

    对于每个结点,它的所有左子树的所有结点都小于这个结点,右子树的所有结点都大于这个结点;

    二叉搜索树的查找:

    从根结点出发,如果查找元素大于这个结点,就往右子树找,否则就往左子树找;

    我们发现二叉搜索树的形态不固定,又因为二叉查找树非常依赖于它的深度,所以用平衡树就能缩短深度;

    它支持区间修改,区间查询;

    主要实现方式有 Splay、Treap 两种;

    平衡树基于一定的操作:旋转(rotate)

    旋转之后,我们会发现 1 往上移了一个深度,我们不断旋转不断往上移,直到移到根,这样我们询问是就可以O(1)询问了;

    Splay

    背景简介:

    伸展树(Splay Tree),是一种二叉搜索树,它能在 O(log n)内完成插入、查找和删除操作。

    它由丹尼尔·斯立特和罗伯特·恩卓·塔扬在 1985 年发明的。

    Splay的特点: 

    在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。 于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

    cnt [ i ]:当前结点 i 的数出现过多少次;

    data [ i ]:当前结点 i 的权值是多少;

    size [ i ]:当前结点 i 及其子树里有多少个数;

    由于 splay 的旋转操作是整个结构的核心,所以我们先研究下 splay 的旋转操作:

    先看一个简单的树,我们将 x 这个结点旋转后应该是这个样子:

    怎么知道旋转后是这个样子的呢?

    splay 的旋转指的是将当前结点旋转到它的父亲结点上去(保证每次旋转这个点的深度-1),那么图中的 x 结点顺时针旋转后就跑到了 y 结点的位置:

    y 结点被赶出来了,只好也顺时针旋转,于是乎跑到了绿点的位置:

    绿点呢?原来它是 y 的右儿子,旋转之后看到 y 的右儿子那里目前还空着,那就接着当 y 的右儿子呗~:

    我们看黄点,它之前是 x 结点的左儿子,x 转过去之后发现 x 的左儿子的位置还空着,那就接着当 x 的左儿子呗~:

    但是 a 就不是很幸运了,x 右儿子的宝座给 y 占了,那怎么办呢?总得给 a 安排个位置吧。

    我们就要在维护 BST 的同时给 a 找一个合适的位置 QwQ~

    根据 BST 的性质可知,a 是小于 y 的,看到旋转之后 y 没有左儿子哎,那就顺理成章的接到 y 的左儿子那里就好了鸭~:

    从上述的旋转过程中我们可以得出一些规律:

    1. 黄点之前作为旋转点 x 的左儿子,旋转之后还是 x 的左儿子;绿点之前作为旋转点 x 的父亲 y 的右儿子,旋转之后还是 y 的右儿子;

    2. 旋转点 x 是父亲 y 的左儿子的时候,那么如果 x 有右儿子,旋转之后要接到 y 的左儿子那里;反之如果旋转点 x 是父亲 y 的右儿子,如果 x 有左儿子,旋转之后要接到 y 的右儿子那里;

    3. 旋转点 x 跑到了父亲 y 的地方;

    4. 旋转点 x 的父亲 y 跑到了 y 的另一边儿子的地方;

    根据上面总结的小规律,然后就可以具体推广一下下啦:

    假如我们一开始并不知道 x 是 y 的左儿子还是右儿子,我们暂且设 x 是 y 的 b 儿子(b 代表左儿子或右儿子);

    1. x 的 b 儿子旋转之后还是 x 的 b 儿子;y 的!b 儿子(另一边的儿子)旋转后还是 y 的 !b 儿子;

    2. x 的 !b 儿子旋转后接到 y 的 b 儿子那里;

    3. x 跑到 y 那里;

    4. y 跑到 !b 儿子那里;

    什么?你说万一 y 不是根结点怎么办。。。

    好说啊!假设 y 的父亲是 z 吧,那么如果之前 y 是 z 的左儿子,旋转之后 x 就是 z 的左儿子;如果之前 y 是 z 的右儿子,旋转之后 x 就是 z 的右儿子(也就是说旋转操作和 z 没什么多大关系);

    真的是脑子里面什么都有,说起来就。。。

    体谅一下本蒟蒻的口才qwq,看不懂肯定是因为我没说清楚~

    那就先看一下旋转操作的代码吧:

    int fa[N],ch[N][2];        //ch[i][0]:i的左儿子,ch[i][1]:i的右儿子 
    int cnt[N];                //结点i的数出现了多少次 
    int data[N];               //结点i的权值 
    int siz[N];                //结点i及子树里有多少个数 
    
    int son(int x)             //看x是他父亲的左儿子还是右儿子 
    {
        return x==ch[fa[x]][1];//左儿子返回0,右儿子返回1 
    }
    
    void pushup(int rt)        //上传 
    {
        siz[rt]=siz[ch[rt][0]]+siz[ch[rt][1]]+cnt[rt];  //左右子树里的结点个数相加并加上当前结点的个数 
    }
    
    void rotate(int x)         //旋转操作 
    {
        int y=fa[x],z=fa[y];   //这里y不一定有父亲,也就是说z可能为0 
        int b=son(x);          //x是y的b儿子,ch[y][b]=x 
        int c=son(y);          //y是z的c儿子,ch[z][c]=y 
        int a=ch[x][!b];       //找x逆儿子a 
        if(z) ch[z][c]=x,fa[x]=z;      //在原来y的位置换上x 
        else root=x;           //如果y没有父亲,说明y就是根,那么旋转后x就是根     
        if(a) fa[a]=y;         //如果a存在,那就把它接到y下面 
        ch[y][b]=a;            //x的逆儿子!b跑到了y的b边 
        ch[x][!b]=y;           //原来x在y的b边,旋转之后y在x的!b那里 
        fa[y]=x;               //y变成了x的儿子 
        pushup(y);             //上传一下 
        pushup(x);
    }

    Splay的伸展: 

    如果当前点,父亲,爷爷呈一条直线,我们先转父亲再转自己。

    如果当前点,父亲,爷爷扭曲,我们连续转两次自己。

    这个东西好像就是要把一个结点 x 旋转到某一层上去吧~

    直接看代码(这个好理解多了):

    void splay(int x,int i)    //Splay操作,我们将x旋转到i的下面(将x旋转成i的儿子) 
    {
        while(fa[x]!=i)        //如果一直没转成i的儿子就一直转 
        {
            int y=fa[x],z=fa[y];  //y是x的父亲,z是y的父亲 
            if(z==i)          //如果i是x的爷爷的话 
            {
                rotate(x);    //我们直接再转一次x就是i的儿子了 
            } 
            else 
            {
                if(son(x)==son(y))  //如果x,y,z同线(同为左孩子或同为右孩子) 
                {
                    rotate(y);//先旋转一下y    
                    rotate(x);//在旋转一下x 
                } else 
                {
                    rotate(x);//旋转两下x 
                    rotate(x);
                }
            }
        }
    }

    插入一个结点(这个和 BST 很相似,也很好理解):

    void insert(int &rt,int x) //插入一个结点 
    {
        if(rt==0)              //原树里没有这个数,我们要新建结点 
        {
            rt=++nn;           //nn是结点个数    
            data[rt]=x;        //赋值 
            siz[rt]=cnt[rt]=1;  
            return;
        }
        if(x==data[rt])        //如果插入的这个数在树种出现过了 
        {
            cnt[rt]++;         //这个数的数量加一 
            siz[rt]++;         //子树内结点个数加一 
            return;
        }
        if(x<data[rt])         //要插入的这个数比当前结点小 
        {
            insert(ch[rt][0],x); //往左子树里面插入 
            fa[ch[rt][0]]=rt;  //tr的左儿子的父亲是rt,这里顺便初始化一下 
            pushup(rt);        //更新一下rt的siz 
        } 
        else
        {
            insert(ch[rt][1],x);//否则就要往右子树里面插入 
            fa[ch[rt][1]]=rt;   //rt的右儿子的父亲是rt,这里顺便初始化一下 
            pushup(rt);         //更新一下rt的siz 
        }
    }

    删除一个权值为 x 的数:

    void del(int rt,int x)      //删除值为x的结点        
    {
        if(data[rt]==x)         //我们找到了这个结点,准备删除它 
        {
            if(cnt[rt]>1)       //如果结点不只一个,减掉一个就好了 
            {
                cnt[rt]--;
                siz[rt]--;
            } 
            else                //如果只有一个 
            {
                splay(rt,0);    //将我们要删除的这个rt结点旋转到根结点(根结点的编号是0) 
                int p=getmn(ch[rt][1]);  //求出大于rt的最小的数(方法是找出右子树的最小值) 
                if(p==-1)       //如果发现右子树里没有左儿子的话,那么右儿子就是最小的 
                {
                    root=ch[rt][0];      //让右儿子作为新树的根 
                    fa[ch[rt][0]]=0;     //左儿子接到右儿子下面,就是根的儿子 
                } 
                else            //如果有左儿子 
                {
                    splay(p,rt);//先将这个最小值旋转到当前结点的儿子那里 
                    root=p;     //最小值作为新根 
                    fa[p]=0;    
                    ch[p][0]=ch[rt][0];  //将当前结点的左儿子接到最小值下面 
                    fa[ch[rt][0]]=p; 
                    pushup(p);  //更新一下根结点的siz 
                }
            }
            return;
        } 
                                //熟悉的寻找x的过程 
        if(x<data[rt])          //如果x小于当前结点就走左子树 
        {
            del(ch[rt][0],x);
        } 
        else 
        {
            del(ch[rt][1],x);   //否则走右子树 
        }
        pushup(rt);
    }

    找最小值(这个和 BST 一毛一样,方法就是一直走左子树):

    int getmn(int rt)           //找最小值       
    {
        int p=rt,ans=-1;
        while(p) 
        {
            ans=p;
            p=ch[p][0];         //有左儿子就一直走左儿子 
        }
        return ans;
    }

    找 x 的前驱:

    int getpre(int rt,int x)    //算x的前驱,前驱是最大的比x小的数 
    {
        int p=rt,ans;           //p是当前结点编号,ans是x的前驱 
        while(p)                 
        {
            if(x<=data[p])      //如果x比当前结点小,走左子树 
            {
                p=ch[p][0];     
            } 
            else                //否则就走右子树 
            {
                ans=p;          //随着我们一直往下往右找,找到的前驱一定是越来越优的 
                p=ch[p][1];    
            }
        }
        return ans;
    }

    找 x 的后继:

    int getsuc(int rt,int x)    //找x的后继,后继就是最小的大于x的数 
    {
        int p=rt,ans;
        while(p) 
        {
            if(x>=data[p])      //比当前结点大走右子树 
            {
                p=ch[p][1];
            } 
            else                //否则走左子树 
            {
                ans=p;          //随着我们一直往下往左走,找到的后继一定越来越优 
                p=ch[p][0];
            }
        }
        return ans;
    }

    找排名第 k 的数是几:

    int getkth(int rt,int k)    //求排名第k的结点 
    {
        int l=ch[rt][0];        //当前结点的左儿子 
        if(siz[l]+1<=k&&k<=siz[l]+cnt[rt]) return data[rt];   //如果比左子树的个数多但是却又比加上该结点后的个数少,那么不就是第k名元素就是当前结点 
        if(k<siz[l]+1) return getkth(l,k);        //比左子树的个数少的话就在左子树里 
        else return getkth(ch[rt][1],k-siz[l]-cnt[rt]);       //否则就在右子树里 
    }

    求权值为 k 的数排名第几:

    int getk(int rt,int k)      //求权值为k的结点排第几            
    {
        if(data[rt]==k)         //我们找到了这个结点 
        {
            splay(rt,0);        //把它转到根的位置,这样的话左儿子个数+1就是它的排名 
            if(ch[rt][0]==0)    //如果没有左儿子,它就排第一 
            {
                return 1;
            } 
            else                //如果有左儿子 
            {
                return siz[ch[rt][0]]+1;  //排名为:左儿子个数+1 
            }
        }
                                                  //又是熟悉的查找过程 
        if(k<data[rt]) return getk(ch[rt][0],k);  //比当前结点小就走左子树 
        if(data[rt]<k) return getk(ch[rt][1],k);  //比当前结点大就走右子树 
    }

    splay其实理解透了就很简单了哦~ 建议先看一下C++提高组一本通的 Treap 部分再来食用效果更佳哦~

    然后就没了鸭QwQ~

  • 相关阅读:
    终端安全管理之殇:安全管控能力与用户体验
    一个人的公众号,我写了1w+
    记录一个终端入网小助手的bug
    Window权限维持(十):Netsh Helper DLL
    Window权限维持(九):端口监视器
    Window权限维持(八):时间服务器
    Window权限维持(七):安全支持提供者
    Window权限维持(六):BITS Jobs
    Window权限维持(四):快捷方式修改
    一个有意思的组合漏洞场景
  • 原文地址:https://www.cnblogs.com/xcg123/p/11187909.html
Copyright © 2020-2023  润新知