zoukankan      html  css  js  c++  java
  • 【瞎口胡/史前巨坑】Treap 学习笔记

    友情提示:这篇博文是写给自己复习的,可能写的比较烂,不建议初学者学习。

    二叉搜索树(二叉排序树,BST)是一种特殊的二叉树。这种二叉树带点权(( ext{key})),且它满足对于任意节点,其左子树中的节点的权值均小于它,右子树中的节点的权值均大于它。

    容易发现,对任意一棵 BST 进行中序遍历,得到的序列递增。

    对上图中的 BST 进行中序遍历,得到 ([1,4,8,9,12,13])

    可以看看这道题。里面的操作 BST 都能做,也有很多博客讲过了。

    容易发现,BST 的期望树高是 (O(log n)) 的,然而,对于一个序列,其对应的 BST 不一定唯一。

    这棵 BST 的中序遍历和第一棵 BST 一样,但是树高增加了很多。对于这种 BST,树高变成了 (O(n)),退化成了暴力,显然不够优秀。

    所以我们需要一些措施来防止 BST 退化。最好写的就是 Treap 啦。

    Treap 中,每个 BST 节点除了点权 ( ext{key}) 还有随机权值 ( ext{rnd})。任意一棵 Treap 需要满足:

    • 以节点的 ( ext{key}) 为点权构成的二叉树符合 BST 的全部性质
    • 以节点的 ( ext{rnd}) 为点权构成的二叉树符合堆的全部性质

    这棵树就是 Treap。但是因为 ( ext{rnd}) 是随机的,最坏情况下仍然会退化。但是出题人总不会对着随机种子卡你啊,所以树高基本上是 (O(log n))

    推荐几个好用的随机种子:

    • (114514)(1919810)(998244353)(20071005)
    • 当然,srand(time(NULL)) 才是王道

    那么这么优秀的平衡树要怎么写呢?

    有两种 Treap,一种 Treap 带旋转,一种是非旋。带旋转的 Treap 难写难调,容易转错;非旋 Treap 好写,不容易出错。

    非旋 Treap,又名 FHQ-Treap。核心操作是两个:split 和 merge。请忘掉上面所有的 BST 操作,基本用不着。

    split

    split 操作将一个 Treap 拆成 (x,y) 两棵,(x) 中所有 ( ext{key} leq v),而 (y) 中所有 ( ext{key} > v)

    void split(int now,int v,int &x,int &y){
    	if(!now){ // 分完了
    		x=y=0;
    		return;
    	}
    	update(now); // 提前更新子树信息
    	if(tree[now].val<=v){ // now 和 now 的左子树分到 x
    		x=now;
    		split(rc(now),v,rc(x),y); // 将 now 的右子树继续拆分
    		update(x);
    	}else{ // now 和 now 的右子树分到 y
    		y=now;
    		split(lc(now),v,x,lc(y));
    		update(y);
    	}
    	return;
    }
    

    还有一种 split 以子树大小划分,中序遍历前 (k) 个分到 (x),剩余部分分到 (y)。这种 split 在维护序列的时候非常有用(可以提取一个序列中的某个区间 ([l,r]))。

    void split(int now,int k,int &x,int &y){
    	if(!now){
    		x=y=0;
    		return;
    	}
    	update(now),pushdown(now); // 一部分序列维护问题需要区间打标记,在 split 前需要把所有标记下传
    	if(tree[lc(now)].size<k){
    		x=now;
    		split(rc(now),k-tree[lc(now)].size-1,rc(x),y);
    		update(x);
    	}else{
    		y=now;
    		split(lc(now),k,x,lc(y));
    		update(y);
    	}
    	return;
    }
    

    merge

    merge 将两棵平衡树 (x,y) 合并成一棵。当 (maxlimits_{a in x} { ext{key}_a } leq maxlimits_{b in y} { ext{key}_b })(x) 里面的任意 ( ext{key}) 不比 (y) 中的大)的时候,这才是对的。

    特殊地,在维护序列的时候,merge 操作的意义是将两个连续的区间合并成一个,但写法并没有改变。

    void merge(int &now,int x,int y){
    	if(!x||!y){
    		now=x|y;
    		return;
    	}
    	update(x),update(y);
    	if(tree[x].rnd<tree[y].rnd){ // 维护堆性质(大根堆小根堆都行啦)
    		now=x;
    		merge(rc(now),rc(x),y);
    		update(now);
    	}else{
    		now=y;
    		merge(lc(now),x,lc(y));
    		update(now);
    	}
    	return;
    }
    

    现在再来看看 这道题

    • 插入 (x)
    inline void Insert(int v){
    	int x,a,b;
    	NewNode(x,v); // 新建节点
    	split(root,v,a,b); 
    	merge(a,a,x);
    	merge(root,a,b); // 记得合并回去
    	return;
    }
    
    • 删除 (x)

    因为有多个 (x) 时只删除一个,所以我们没办法直接将 ( ext{key}) 等于 (x) 的子树提出来扔掉。将子树提出来之后,应该合并子树根的左右儿子成为一棵新子树,相当于消除了根节点。最后合并回去,就好啦。

    inline void Delete(int v){
    	int a,b,c;
    	split(root,v,a,c);
    	split(a,v-1,a,b);
    	merge(b,lc(b),rc(b));
    	merge(a,a,b);
    	merge(root,a,c);
    	return;
    }
    
    • 查询 (x) 的排名

    将小于 (x) 的子树提出来,再加上 (1) 就是答案。

    inline int Rank(int x){
    	int a,b;
    	split(root,x-1,a,b);
    	int ans=tree[a].size+1;
    	merge(root,a,b);
    	return ans;
    }
    
    • 求第 (x) 大的数

    BST 基本操作。

    inline int Kth(int x,int k){
    	assert(tree[x].size>=k); // 如果以 x 为根的子树大小 <= k,那一定传参数的时候写假了,抛出 RE 来检查
    	while(1){
    		if(k<=tree[lc(x)].size){
    			x=lc(x);
    		}else if(k==tree[lc(x)].size+1){
    			return tree[x].val;
    		}else{
    			k-=tree[lc(x)].size+1;
    			x=rc(x);
    		}
    	}
    	return 114514; // 当然,这里是永远也跑不到的,但是控制流达到非 void 函数末尾会报错...
    }
    
    • (x) 的前驱

    将小于 (x) 的子树提出来,子树最大值就是答案。子树最大值可以 kth 求。

    inline int GetPre(int x){
    	int a,b;
    	split(root,x-1,a,b);
    	int ans=Kth(a,tree[a].size);
    	merge(root,a,b);
    	return ans;
    }
    
    • (x) 的后继。

    同上。

    完整代码:

    # include <bits/stdc++.h>
    # define rr register
    const int N=100010;
    struct Node{
    	int val,son[2],rnd,size;
    }tree[N];
    int root,cnt;
    inline int &lc(int x){
    	return tree[x].son[0];
    }
    inline int &rc(int x){
    	return tree[x].son[1];
    }
    inline void NewNode(int &x,int v){
    	x=++cnt;
    	tree[x].rnd=rand(),tree[x].size=1,tree[x].val=v;
    	return;
    }
    inline void update(int x){
    	tree[x].size=tree[lc(x)].size+tree[rc(x)].size+1;
    	return;
    }
    void split(int now,int v,int &x,int &y){
    	if(!now){
    		x=y=0;
    		return;
    	}
    	update(now);
    	if(tree[now].val<=v){
    		x=now;
    		split(rc(now),v,rc(x),y);
    		update(x);
    	}else{
    		y=now;
    		split(lc(now),v,x,lc(y));
    		update(y);
    	}
    	return;
    }
    void merge(int &now,int x,int y){
    	if(!x||!y){
    		now=x|y;
    		return;
    	}
    	update(x),update(y);
    	if(tree[x].rnd<tree[y].rnd){
    		now=x;
    		merge(rc(now),rc(x),y);
    		update(now);
    	}else{
    		now=y;
    		merge(lc(now),x,lc(y));
    		update(now);
    	}
    	return;
    }
    inline int Kth(int x,int k){
    	assert(tree[x].size>=k);
    	while(1){
    		if(k<=tree[lc(x)].size){
    			x=lc(x);
    		}else if(k==tree[lc(x)].size+1){
    			return tree[x].val;
    		}else{
    			k-=tree[lc(x)].size+1;
    			x=rc(x);
    		}
    	}
    	return 114514;
    }
    inline void Insert(int v){
    	int x,a,b;
    	NewNode(x,v);
    	split(root,v,a,b);
    	merge(a,a,x);
    	merge(root,a,b);
    	return;
    }
    inline void Delete(int v){
    	int a,b,c;
    	split(root,v,a,c);
    	split(a,v-1,a,b);
    	merge(b,lc(b),rc(b));
    	merge(a,a,b);
    	merge(root,a,c);
    	return;
    }
    inline int Rank(int x){
    	int a,b;
    	split(root,x-1,a,b);
    	int ans=tree[a].size+1;
    	merge(root,a,b);
    	return ans;
    }
    inline int GetPre(int x){
    	int a,b;
    	split(root,x-1,a,b);
    	int ans=Kth(a,tree[a].size);
    	merge(root,a,b);
    	return ans;
    }
    inline int GetNext(int x){
    	int a,b;
    	split(root,x,a,b);
    	int ans=Kth(b,1);
    	merge(root,a,b);
    	return ans;
    }
    inline int read(void){
    	int res,f=1;
    	char c;
    	while((c=getchar())<'0'||c>'9')
    		if(c=='-')f=-1;
    	res=c-48;
    	while((c=getchar())>='0'&&c<='9')
    		res=res*10+c-48;
    	return res*f;
    }
    void print(int x){
    	if(!x)
    		return;
    	putchar('['),print(lc(x)),printf("%d(%d)",tree[x].val,tree[x].size),print(rc(x)),putchar(']');
    	return;
    }
    int main(void){
    	int n=read();
    	while(n--){
    		int opt=read();
    		switch(opt){
    			case 1:{
    				Insert(read());
    				break;
    			}
    			case 2:{
    				Delete(read());
    				break;
    			}
    			case 3:{
    				printf("%d
    ",Rank(read()));
    				break;
    			}
    			case 4:{
    				printf("%d
    ",Kth(root,read()));
    				break;
    			}
    			case 5:{
    				printf("%d
    ",GetPre(read()));
    				break;
    			}
    			case 6:{
    				printf("%d
    ",GetNext(read()));
    				break;
    			}
    			default:{
    				break;
    			}
    		}
    	}
    	
    	return 0;
    }
    
  • 相关阅读:
    One网络模拟器探索之七:路由模块的工作原理
    An Intuitive Explanation of GraphSAGE
    《k8s 源码分析》- Custom Controller 之 Informer
    《k8s-1.13版本源码分析》-抢占调度
    《k8s-1.13版本源码分析》- Informer 机制
    《k8s-1.13版本源码分析》-调度器初始化
    《k8s-1.13版本源码分析》-源码调试
    《k8s-1.13版本源码分析》-调度优选
    《k8s-1.13版本源码分析》-调度预选
    《k8s-1.13版本源码分析》-调度器框架
  • 原文地址:https://www.cnblogs.com/liuzongxin/p/13574361.html
Copyright © 2011-2022 走看看