【算法学习】Splay入门

Splay

变量声明

f [ i ] 表示 i 的父节点
c h [ i ] [ 0 ] 表示 i 的左儿子, c h [ i ] [ 1 ] 表示 i 的右儿子
k e y [ i ] 表示 i 的关键字(相当于map中的first)
c n t [ i ] 表示 i 节点的关键字出现次数(相当于map中的second)
s i z e [ i ] 表示包括 i 的子树的大小
s z 表示整棵树的大小, r o o t 表示整棵树的根

基本操作

clear

将当前节点的各项值都清空

inline void clear(int x){
    ch[x][0] = ch[x][1] = cnt[x] = key[x] = f[i] = size[i] = 0;
}

get操作

判断当前节点是否是它父节点的右孩子,其实也就是返回一个点是其父亲节点的左孩子还是右孩子。如果是左孩子就为0,如果是右孩子就为1。这和我们的规定是一致的。

inline int get(int x){
    return ch[fa[x]][1] == x;
}

update操作

在修改之后用于更新当前节点的 s i z e

inline void update(int x){
    if(x){
        size[x] = cnt[x];
        if(ch[x][0]) size[x] += size[ch[x][0]];
        if(ch[x][1]) size[x] += size[ch[x][1]];
    }    
}

复杂操作

rotate操作

首先定义几个变量:
f a = f [ x ] 要旋转的节点 x 的父亲
p a = f [ f a ] 要旋转的节点 x 父亲的父亲
w h i c h = g e t ( x ) 要旋转的节点 x 位于是他父亲的哪个儿子
s o n = c h [ x ] [ w h i c h \^ 1 ] 要旋转节点 x 的另外一侧的儿子

有了上面变量的基础,就可以进行旋转操作了,这里从下面的节点依次向上修改父子关系,也就是从 s o n 节点开始。值得注意的是,父子关系是靠父亲的儿子和儿子的父亲这两层关系维护的,不妨把他们看做是两个方向相反的指针,所以我们在修改的时候,要成对的修改这些关系

首先是初始状态如下图 [图1]
这里写图片描述
然后我们修改最下面的节点关系,即f[son]=fa,f[fa][which]=son,这样他们之间的关系就形成了图2[图2]
这里写图片描述
此时注意到,关系之中有两条是单向关系,这就不是很nice,接下来我们就来修改这些单向关系,即ch[x][which^1]=fa,f[fa] = x,这样就单向关系就被修正好了,变成图3[图3]
这里写图片描述
接着就差一点点,我们只需要修改被旋转节点父亲,这个不难,一步就完成了,也就是f[x] = pa。变成下面的图4[图4]
这里写图片描述
到这里还不能结束,因为pa为空固然正确,但是如果不是空,就很捉急。所以我们要判断一下是不是空,如果不是空,要求改pa的儿子关系了,也就是说,ch[pa][ch[pa][1] == fa] = x,这里有一点点绕,要理解一下。

如果做完这一步,那就差更新操作啦,更新完了就好啦。不过这里还要注意,要从下往上更新,也就是先更新被转下去的fa,然后再更新被转上去的x,保证正确性。
这里写图片描述

inline void rotate(ine x){
    int fa = f[x],pa = f[fa],which = get(x);
    int son = ch[x][which^1];
    f[son] = fa, ch[fa][which] = son;
    ch[x][which^1] = fa, f[fa] = x;
    f[x] = pa;
    if(pa) ch[pa][ch[pa][1] == fa] = x;
    update(fa); update(x);
}

Splay操作

有了rotate操作之后,splay 操作就不难实现了。Splay操作所要实现的,即把一个结点x旋转到根节点,唯一需要判断的情况,也只要当x和fa还有pa在一条直线上的时候,我们需要先对fa进行旋转,然后再对x进行旋转,代码如下。

inline void splay(int x) {
    for(int fa;f[x] = fa;rotate(x))
        if(f[fa]) rotate(get(x) == get(fa)?fa:x);
    root = x;   
}

insert操作

insert操作其实和二叉排序树的查找很类似。

我们知道二叉排序树查找,是比较当前查找值和节点值的大小,如果查找值比节点值小,那么去节点的左子树继续查找,否则去节点的右子树进行查找。这里也是类似的,不过需要考虑几种情况罢了:

  1. 如果当前树为空,那么显然直接放到跟节点就好了。
  2. 如果当前树不为空,按照二叉排序数的规则继续找应该插入的位置。有可能节点值和要插入的值相同,那么就将这个节点的大小增加一,然后执行更新操作。注意先更新本身,再更新父亲,这样保证正确性。最后我们将节点旋转到根。旋转的原因是,对这个节点进行了一次更新,就认为他很可能后面会持续访问,所以旋转到根。离根越近查询时间越短。
  3. 如果树不为空,并且在插入操作的时候还没有找到和其权值相等的节点,这就意味着需要新建一个节点了。具体操作和当数为空的时候很类似,区别在于,需要维护父子关系,就要对两层指针进行修改。首先新建节点的指针很好修改,f[sz]=fa就完成了,但是对于父亲左右儿子维护,就要判断大小,如果小就在左子树,否则就是右子树。也就是ch[fa][key[fa]>v] = x这样就好了,仍然注意需要更新父亲节点,然后把新建的节点旋转到根。

从上面的操作可以看到,如果有新建的节点,考虑是否是根,如果是根那么不需要维护父子关系,否则需要维护父子关系

inline void insert(int v){
    if(root == 0){
        sz++; ch[sz][0] = ch[sz][1] = f[sz] = 0;
        key[sz] = v, cnt[sz] = size[sz] = 1, root = sz;
        return;
    }
    int now = root, fa = 0;
    while(true){
        if(key[now] == v) {cnt[now]++; update(now); update(fa); splay(now); return;}
        fa = now, now = key[now]<v?ch[now][0]:ch[now][1];
        if(now == 0){
            sz++; ch[sz][0] = ch[sz][1] = 0;
            key[sz] = v, cnt[sz] = size[sz] = 1;
            f[sz] = fa, ch[fa][v>key[fa]] = sz;
            update(fa); splay(sz); return;
        }
    }
}

find操作

inline int find(int x){
    int ans = 0,now = root;
    while(true){
        if(v<key[now]) now = ch[now][0];
        else{
            int temp = (ch[now][0]?size[ch[now][0]]:0);
            if(v == key[now]){ans += temp + 1; splay(now); return ans;}
            asn += temp + cnt[now];now = ch[now][1];
        }
    }
}

findx操作

findx操作用来查询当前权值排名x的点权值。

inline int findx(int x){
    int now = root;
    while(true){
        if(ch[now][0] && x <= size[ch[now][0]]) now = ch[now][0];
        else{
            int temp = (ch[now][0]?size[ch[now][0]]:0)+cnt[now];
            if(x<=temp) return key[now];
            x-=temp;now=ch[now][1];
        }    
    }

}

next & pre操作

求一个节点的前驱和后继,根据二叉排序树的性质,一个节点的前驱,是节点的左子树的最右边的一个节点,而一个节点的后继是一个节点右子树最左边的一个节点。

但是需要找到这个节点,然后再查找。我们也可以换种思路,求前驱或者后继的时候,先将这个节点插入,注意到插入后节点会被旋转到根,那么接下来的前驱或者后继都是对于根节点来说的,这就很好办了。

这种方法的缺点是常数会比较大。

inline int pre(int x){
    int now = ch[root][0];
    while(ch[now][1]) now = ch[now][1];
    return now;
}
inline int nxt(int x){
    int now = ch[root][1];
    while(ch[now][0]) now = ch[now][0];
    return now;
}

del操作

需要分情况讨论。

  1. 根节点的的大小大于1,那么节点大小减一即可
  2. 根节点没有左右子树,并且节点大小仍为1,直接清除即可。
  3. 根节点只有左子树或者右子树,并且根节点大小为1,那么就将根节点删除,把剩余的一个子树接过去即可。注意要修改父子关系。
  4. 根节点既有左子树又有右子树,并且根节点大小为1,就把根节点的前驱放到根的位置。这样原来的根节点一定是新根的右子树上的第一个节点,这样只需要修改这两个节点的父子关系即可。

需要注意的是,在splay之后不需要修改根节点的父亲,即f[root] = 0这种操作在splay后是多余的,因为splay操作维护了这个更改。

inline void del(int x){
    find(x);
    if(cnt[root] > 1) {cnt[root]--; size[root]--; return;}
    if(!ch[root][0] && !ch[root][1]) {clear(root);root=0;return;}
    if(!ch[root][0]) {int oldroot = root; root = ch[oldroot][1];f[root] = 0; clear(oldroot); return;}
    if(!ch[root][1]) {int oldroot = root; root = ch[oldroot][0];f[root] = 0; clear(oldroot); return;}
    int lbig = pre(), oldroot = root;
    splay(lbig); // => root = lbig
    f[ch[oldroot][1]] = root, ch[root][1] = ch[oldroot][1];
    clear(oldroot); update(root);
    return;
}

猜你喜欢

转载自blog.csdn.net/pengwill97/article/details/81006903