• 浅谈替罪羊树


    替罪羊树 学习总结

    前言:

    为什么会学替罪羊树?因为觉得AVL树那些的左旋右旋什么的太晕了啊QAQ

    所以就在RHL大佬的推荐下,学习起了替罪羊树,这种不用旋转操作就能维护平衡的树


    知识介绍:

    在OI界一直都会有这样的一句话:“暴力即优雅”,而诸如分块、替罪羊树则是对这句话的最好诠释

    对于二叉搜索树,最重要的就是维护树的平衡,将时间复杂度保持在O(logN)左右,使其不会退化成一条链,从而到时时间复杂度增长到O(N)

    • 在替罪羊树上,插入或删除节点的平摊最坏时间复杂度是O(logN),搜索节点的最坏时间复杂度是 O(logN)

    像AVL树、Splay一类的树都是通过旋转来维持平衡,而替罪羊树呢,则是简单粗暴的“不平衡?那就拍扁重建!

    什么意思?让我们通过一些问答来理解一下

    • 什么时候拍扁重建?

    在每次插入点后,判断当前子树是否平衡,如果不平衡则拍扁重建

    • 怎么判断是否平衡?

    如果一棵树的左子树/右子树的存在的节点数量 > 这棵树的存在的节点数量× alpha,那么就要拍扁重建

    • alpha是什么?

    alpha是我们人为选择的一个平衡因子,在0.5-1之间,一般选择0.7或0.8


    基础操作:

    在了解了替罪羊树的基础知识以后,让我们来学习一下替罪羊树的基本操作

    • 一些变量
    intn,x,op,mep,tep,root,tmp[2000005],mem[2000005];
    
    //tmp拍扁的时候用的内存空间
    //mep指向内存池mem[]的指针
    //tep指向拍扁时用的tmp[]的指针
    
    struct node {
    	int lc,rc,v,valid,total; //valid子树未被删除的点数,total子树总点数 
    	bool pd;  //是否被删除:1表示未被删除,0表示被删除 
    } a[2000005];
    
    
    • 判断是否拍扁重建
    inline bool flag(int now) { //判断是否需要平衡一下 
    	if((double)a[now].valid*alpha<=(double)max(a[a[now].lc].valid,a[a[now].rc].valid)) return true;
    	return false;
    }
    
    
    • 建树&调整整棵树
    inline void build(int l,int r,int &now) { //建树&调整维护
    	int mid=(l+r)>>1;
    	now=tmp[mid]; //tmp里存的是编号:把中间的元素取出来,中间元素的编号为now
    	if(l==r) {
    		a[now].lc=a[now].rc=0; //新插入的节点都为叶子节点,进行初始化 
    		a[now].total=a[now].valid=1;
    		return;
    	}
    	if(l<mid) build(l,mid-1,a[now].lc); //mid已经建完了,建左右子树 
    	else a[now].lc=0; //l==mid,则没有左儿子,但此时r那个节点作为了mid节点的右儿子 
    	build(mid+1,r,a[now].rc);
    	/* 因为mid总是(l+r)>>1向下取整,所以只需要判断l是小于mid还是等于mid,而ri永远大于mid */
    	a[now].total=a[a[now].lc].total+a[a[now].rc].total+1; //更新节点信息 
    	a[now].valid=a[a[now].lc].valid+a[a[now].rc].valid+1;
    }
    
    
    • DFS求拍扁的顺序

    我们来看一看替罪羊树是怎么拍扁需要重构的树的,如下草图:

    我们可以发现拍扁后的序列其实是已经排好序的,而这个顺序就是对这棵重建子树的中序遍历,所以我们重建前需要dfs一下

    inline void dfs(int now) { //中序遍历(左根右),找出要被拍扁的节点的编号 
    	if(!now) return; //叶子节点 
    	dfs(a[now].lc);
    	if(a[now].pd==1) tmp[++tep]=now; //加入到拍扁的时候用的数组里存放(pd是惰性删除) 
    	else mem[++mep]=now;
    	dfs(a[now].rc);
    }
    
    
    • 重建
    inline void rebuild(int &now) {
    	tep=0; //重建的子树要从头开始算
    	dfs(now); //dfs找到重建的顺序 
    	if(tep) build(1,tep,now);
    	else now=0;
    }
    
    • 插入一个数

    替罪羊树在插入时,是一边向下一边更新,这也是与其他树不同的地方

    inline void insert(int &now,int k) {
    	if(!now) { //找到一个插入的位置 
    		now=mem[mep--];
    		a[now].v=k;
    		a[now].pd=a[now].total=a[now].valid=1;
    		a[now].lc=a[now].rc=0;
    		return;
    	}
    	a[now].total++; //一边向下一边更新
    	a[now].valid++;
    	if(a[now].v>=k) insert(a[now].lc,k);
    	else insert(a[now].rc,k);
    	if(flag(now)==true) rebuild(now); //从下往上重建会更快(因为下面的子树小,好操作) 
    }
    
    • 查询数k的排名
    inline int findth(int k) { //查找值为k的排名 
    	int now=root;
    	int ans=1;
    	while(now) {
    		if(a[now].v>=k) now=a[now].lc;
    		else {
    			ans+=a[a[now].lc].valid+a[now].pd; //+a[now].pd是因为相同大小的节点虽然放在一起,但是我不知道这个节点上相同的是不是还存在啊..所以得单独加该节点..至于valid是除现节点以外的子树大小。
    			now=a[now].rc;
    		}
    	}
    	return ans;
    }
    
    • 查询排名为k的值
    inline  int findn(int k) { //查找排名为k的值 
    	int now=root;
    	while(now) {
    		if(a[now].pd&&a[a[now].lc].valid+1==k) return a[now].v;
    		else if(a[a[now].lc].valid>=k) now=a[now].lc;
    		else {
    			k-=a[a[now].lc].valid+a[now].pd;
    			now=a[now].rc;
    		}
    	}
    }
    
    • 删除值为k的的数

    这里是通过转换为:先求值k的排名,再删除排名为k的数

    注意一下,这里的删除都是惰性删除,即给删除的点打上标记

    真正的删除是在DFS那里进行的

    同时,删除之后我们也要判断一下是否需要重建(这里的判断条件与之前有略微不同)

    inline void deleth(int &now, int k) { //删除排名为k的数
    	if(a[now].pd&&a[a[now].lc].valid+1==k) {
    		a[now].pd=0;
    		a[now].valid--;
    		return;
    	}
    	a[now].valid--;
    	if(a[a[now].lc].valid+a[now].pd>=k) deleth(a[now].lc,k); 
    	else deleth(a[now].rc,k-a[a[now].lc].valid-a[now].pd);
    }
    
    inline void deletn(int k) { //删除值为k的数
    	deleth(root, findth(k));
    	if((double)a[root].total*alpha>a[root].valid) rebuild(root); //删太多也重建一下
    }
    

    例题:

    (1)洛谷P3369 基础版普通平衡树

    (2)洛谷P6136 加强版普通平衡树


    完整代码:

    现在来一发没有注释的Code

    PS:以下给出的是例题1的代码(例题2的代码只需要在主程序上更改以下即可,文末会给出)

    #include <bits/stdc++.h>
    #define alp 0.8
    using namespace std;
    int n,x,op,mep,tep,root,tmp[2000005],mem[2000005];
    
    struct node {
    	int lc,rc,v,valid,total;
    	bool pd;
    } a[2000005];
    
    inline bool flag(int now) {
    	if((double)a[now].valid*alp<=(double)max(a[a[now].lc].valid,a[a[now].rc].valid)) return true;
    	return false;
    }
    
    inline void build(int l,int r,int &now) {
    	int mid=(l+r)>>1;
    	now=tmp[mid];
    	if(l==r) {
    		a[now].lc=a[now].rc=0;
    		a[now].valid=a[now].total=1;
    		return ;
    	}
    	if(l<mid) build(l,mid-1,a[now].lc);
    	else a[now].lc=0;
    	build(mid+1,r,a[now].rc);
    	a[now].total=a[a[now].lc].total+a[a[now].rc].total+1;
    	a[now].valid=a[a[now].lc].valid+a[a[now].rc].valid+1;
    }
    
    inline void dfs(int now) {
    	if(!now) return ;
    	dfs(a[now].lc);
    	if(a[now].pd==1) tmp[++tep]=now;
    	else mem[++mep]=now;
    	dfs(a[now].rc);
    }
    
    inline void rebuild(int &now) {
    	tep=0;
    	dfs(now);
    	if(tep) build(1,tep,now);
    	else now=0;
    }
    
    inline void insert(int &now,int k) {
    	if(!now) {
    		now=mem[mep--];
    		a[now].v=k;
    		a[now].lc=a[now].rc=0;
    		a[now].pd=a[now].valid=a[now].total=1;
    		return ;
    	}
    	a[now].total++;
    	a[now].valid++;
    	if(a[now].v>=k) insert(a[now].lc,k);
    	else insert(a[now].rc,k);
    	if(flag(now)==true) rebuild(now);
    }
    
    inline int findth(int k) {
    	int now=root;
    	int ans=1;
    	while(now) {
    		if(a[now].v>=k) now=a[now].lc;
    		else {
    			ans+=a[a[now].lc].valid+a[now].pd;
    			now=a[now].rc;
    		}
    	}
    	return ans;
    } 
    
    inline int findn(int k) {
    	int now=root;
    	while(now) {
    		if(a[now].pd&&a[a[now].lc].valid+1==k) return a[now].v;
    		else if(a[a[now].lc].valid>=k) now=a[now].lc;
    		else {
    			k-=a[now].pd+a[a[now].lc].valid;
    			now=a[now].rc;
    		}
    	}
    }
    
    inline void deleth(int &now,int k) {
    	if(a[now].pd&&a[a[now].lc].valid+1==k) {
    		a[now].pd=0;
    		a[now].valid--;
    		return ;
    	}
    	a[now].valid--;
    	if(a[a[now].lc].valid+a[now].pd>=k) deleth(a[now].lc,k);
    	else deleth(a[now].rc,k-a[a[now].lc].valid-a[now].pd);
    }
    
    inline void deletn(int k) {
    	deleth(root,findth(k));
    	if((double)a[root].total*alp>a[root].valid) rebuild(root);
    }
    
    int main() {
    	for(register int i=2000000;i>=1;i--) mem[++mep]=i;
    	scanf("%d",&n);
    	for(register int i=1;i<=n;i++) {
    		scanf("%d%d",&op,&x);
    		if(op==1) insert(root,x);
    		if(op==2) deletn(x);
    		if(op==3) printf("%d
    ",findth(x));
    		if(op==4) printf("%d
    ",findn(x));
    		if(op==5) printf("%d
    ",findn(findth(x)-1));
    		if(op==6) printf("%d
    ",findn(findth(x+1)));
    	}
    	return 0;
    }
    
    • 例题2的主程序部分(其他的函数部分和以上一致):
    int main() {
    	for(register int i=2000000;i>=1;i--) mem[++mep]=i;
    	n=read();
    	m=read();
    	for(register int i=1;i<=n;i++) {
    		x=read();
    		insert(root,x);
    	}
    	for(register int i=1;i<=m;i++) {
    		op=read();
    		x=read();
    		x^=last;
    		if(op==1) insert(root,x);
    		if(op==2) deletn(x);
    		if(op==3) {
    			ans^=findth(x);
    			last=findth(x);
    		}
    		if(op==4) {
    			ans^=findn(x);
    			last=findn(x);
    		}
    		if(op==5) {
    			ans^=findn(findth(x)-1);
    			last=findn(findth(x)-1);
    		}
    		if(op==6) {
    			ans^=findn(findth(x+1));
    			last=findn(findth(x+1));
    		}
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    后序:

    终于总结完了!替罪羊树也算是入门了吧?

    嗯..不过还需要多做题巩固应用,AVL树和Splay也得找时间学习

    那....那就继续加油叭qvq

    最后,如果有任何问题,欢迎指出,我们一起进步


  • 相关阅读:
    R语言:常用统计检验
    用R语言的quantreg包进行分位数回归
    使用adagio包解决背包问题
    手机上的微型传感器
    JS常用字符串、数组的方法(备查)
    Threejs 纹理贴图2--凹凸贴图、法线贴图
    Three.js 纹理贴图1--旋转的地球
    Three.js 帧动画
    Three.js光源、相机知识梳理
    Three.js 点、线、网络模型及材质知识梳理
  • 原文地址:https://www.cnblogs.com/Eleven-Qian-Shan/p/13143290.html
Copyright © 2020-2023  润新知