FHQ非旋平衡树

目录

平衡树的灵魂:

平衡树的核心操作:

 split:

merge:

对核心操作运用(split与merge)的例题:

        思路:

        代码:     

无旋 treap 的区间操作:

         

        建树:

        区间翻转:

        其他区间操作:

        CF702F T-Shirts

        题面:

        思路:

        code:



平衡树的灵魂:

        在本人看来,就是rand()的运用了,其保证了其平衡性

        按二叉搜索树的性质,一棵树的形状可能如图1所示:

        

                                                                图1

查询等操作需n的时间,n即点的数量

加上rank(rank由rand()随机赋值),令rank小的为rank大的祖先,可得图2

                                                                         图2

这样,log n就可以解决问题了


平衡树的核心操作:


 split:


           平衡树的分裂,可以按各种关键值,这里为了方便,拿按值分裂举例

           选定一个值v,小于等于v的分出一颗子树,大于v的为一棵子树

           看代码之前,我们先画图感受一下分裂过程,如图3

                                                                图3

关键在于明白为什么右子树也有一部分小于v

//sz为树的大小,val为树根的值,ls rs为树的左右儿子
void pushup(int rt){
    sz[rt]=sz[ls[rt]]+sz[rs[rt]]+1;
}

void split(int rt,int v,int &x,int &y){//x,y为分出的子树的树根
    if(!rt){ x=y=0 ; return ;}//无树可分
    if(val[rt]<=v){
        x=rt;
        split(x,v,rs[x],y);
    }else{
        y=rt;
        split(y,v,x,ls[y]);
    }
    pushup(rt);
}

merge:

        平衡树合并时要保证rank的升序,val的大小关系一般已经知道(注:由于合并操作一般在分裂后马上进行,故分裂出的以x为树根的子树的val小于以y为树根的val)

        

//rk代表rank
void merge(int x,int y){
    if( !x || !y ){ return x|y ;} // 其中一个树为空,则直接返回不为空的树
    if( rk[x]<rk[y] ){
        x=merge(rs[x],y);//y的val大于x的,为x的右子树
        pushup(x);
        return x;
    }else{
        y=merge(x,ls[rt]);//x的val小于y的,为y的左子树
        pushup(y);
        return y;
    }
}

        有了split与merge后,许多操作就只是对它们的运用而已,

对核心操作运用(split与merge)的例题:

        洛谷 P3369 【模板】普通平衡树

        洛谷 P2596 [ZJOI2006] 书架

        洛谷 P1110 [ZJOI2007] 报表统计

        洛谷 P1486 [NOI2004] 郁闷的出纳员

        洛谷 P2161 [SHOI2009] 会场预约

        洛谷 P2584 [ZJOI2006] GameZ游戏排名系统

        这里我讲一讲GameZ游戏排名系统

        思路:

                split 时不再是单一的比较,首先比较玩家的游戏得分,然后是时间;

                

//x代表当前比较的树根的编号,v为分裂的依据
struct st{
	int val,tim;
};

bool judge(int x,st v){
	if(val[x].val==v.val) return val[x].tim<=v.tim;
	else return val[x].val>v.val;
}

void split(int rt,st v,int &x,int &y){
	if(rt==0) { x=y=0; return ;}
	if(judge(rt,v)){
		 x=rt;
		 split(rs[rt],v,rs[rt],y);
	}
	else{
		 y=rt;
		 split(ls[rt],v,x,ls[rt]);
	}
    pushup(rt);
}

                 弄清这个后其余就是基操了,嘿嘿

        

        代码:     

#include <bits/stdc++.h>
#define re register int
#define INF 0x3f3f3f3f
#define N 260000
using namespace std;
int read(){
	int x = 0,f = 1;char ch = getchar();
	while (ch < '0' || ch>'9'){
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9'){
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar();
	}
	return x * f;
}
int n;
struct st{
	int val,tim;
};
map<string,st>pl;
map<pair<int,int>,string>pls;
struct Tree{
	int root,tot,rk[N],ls[N],rs[N],sz[N];
	st val[N];
	
	void pushup(int rt){sz[rt]=sz[ls[rt]]+sz[rs[rt]]+1; }
	
	int getnew(st v){
		val[++tot]=v;
		sz[tot]=1,rk[tot]=rand();
		return tot;
	}
	
	bool judge(int x,st v){
		if(val[x].val==v.val) return val[x].tim<=v.tim;
		else return val[x].val>v.val;
	}
	
	void split(int rt,st v,int &x,int &y){
		if(rt==0) { x=y=0; return ;}
		 if(judge(rt,v)){
		 	x=rt;
		 	split(rs[rt],v,rs[rt],y);
		 }
		 else{
		 	y=rt;
		 	split(ls[rt],v,x,ls[rt]);
		 }
		 pushup(rt);
	}
	
	int merge(int x,int y){
		if(!x||!y) return x|y;
		if(rk[x]<rk[y]){
			rs[x]=merge(rs[x],y);
			pushup(x);
			return x;
		}
		else{
			ls[y]=merge(x,ls[y]);
			pushup(y);
			return y;
		}
	}
	
	void insert(st v){
		int x,y;
		split(root,v,x,y);
		root=merge(merge(x,getnew(v)),y);
	}
	
	void remove(st v){
		int x,y,z;
		split(root,v,x,y);
		v.tim--;
		split(x,v,x,z);
		root=merge(x,y);
	}
	
	int getrank(st v){
		int x,y;
		v.tim--;
		split(root,v,x,y);
		int ans=sz[x]+1;
		root=merge(x,y);
		return ans;
	}
	
	int getsz(){return sz[root];}
	
	string find(int rank){
		int rt=root;
		while(rt){
			if(sz[ls[rt]]+1==rank) break;
			else if(sz[ls[rt]]>=rank) rt=ls[rt];
			else rank-=(sz[ls[rt]]+1),rt=rs[rt];
		}
		return pls[make_pair(val[rt].val,val[rt].tim)];
	}
}tree;
int main(){
   	n=read();
   	for(int i=1;i<=n;i++){
   		string s;
		cin>>s;
		if(s[0]=='+'){
			s.erase(s.begin());
			st w;
			w.val=read(),w.tim=i;
			if(pl.count(s))
				tree.remove(pl[s]);
			pl[s]=w;
			pls[make_pair(w.val,w.tim)]=s;
			tree.insert(w);
		}
		else{
			s.erase(s.begin());
			if(s[0]<='Z'&&s[0]>='A') printf("%d\n" ,tree.getrank(pl[s]));
			else{
				int num=0;
				for(int j=0;j<s.size();j++) num=num*10+s[j]-'0'; 
				for(int j=0;j<10;j++){
					if(tree.getsz()<num+j) break;
					cout<<tree.find(num+j)<<" ";
				}
				printf("\n");
			}
		}
	}
}

无旋 treap 的区间操作:

         

        建树:

                在建树或者插入点的时候,我们可以用暴力,一个个加,时间复杂度O(n log n)
        但在更具难度的题目中,要插入的点可能很多,所以我们需要更优秀的算法
                容易想到将要插入的点变成一条长链插入,但是若只是普通的链,难免在merge中被分           裂开(merge在rank的影响下,读者自己想想原因),这样就跟一个个插入没有区别了,              所以我们需要构造一条在merge不会多余操作的神仙链,

     

int build(int a[],int siz){//a[]: 插入的点集  siz:点集的大小 
	stack<int>s;//使用单调栈,保证rank递增 
	int rt,last; //last: 链头 
	for(int i=1;i<=siz;i++){
		rt=getnew(a[i]);//新建点 
		last=0;
		while(!s.empty() && rk[s.top()]>rk[rt]){//在加入rt前,将rk大于rk[rt]的弹出,保证加入rt后栈保持单调递减 
			last=s.top();
			pushup(last);
			s.pop();
		}
		if(!s.empty()) rs[s.top()]=rt;//为了保证中序遍历不变(即 仍是原数组的顺序),我们让rt为是s.top()的右儿子 
		ls[rt]=last;   				  //rt的左儿子为链 
		s.push(rt); 	
	}
	while(!s.empty()) {//处理掉栈内剩余的点即可 
		last=s.top();
		pushup(last),s.pop();
	}
	return last;//返回链头 
}
//构造链 O(n),而后 merge 为 O(log n)
//总复杂度为 O(n+log n) ,即 O(n) 

        区间翻转:


         无旋 treap 相比旋转 treap 的一大好处就是可以实现各种区间操作,下面我们以文艺平衡树的 模板题 为例,介绍 treap 的区间操作。
        

        在这道题目中,我们需要实现的是区间翻转,那么我们首先需要考虑如何建树,建出来的树需要是初始的区间。

        我们只需要把区间的下标依次插入 treap 中,这样在中序遍历(先遍历左子树,然后当前节点,最后右子树)时,就可以得到这个区间。如图4,序列为(3,2,4,7,1,6,5)

        

                                                                        图4        

        若翻转区间[2,6],序列变为(3,6,1,7,4,2,5),恰好为交换4节点的左右儿子的中序遍历,如图5

                                                                           图5      

        在线段树中,我们一般在更新和查询时下传懒标记。这是因为,在更新和查询时,我们想要更新/查询的范围不一定和懒标记代表的范围重合,所以要先下传标记,确保查到和更新后的值是正确的。这时我们就需要pushdown来下传标记了

//turn为翻转的标记
    void down_turn(int rt){
        swap(ls[rt],rs[rt]);
        turn[rt]^=1;
    }
    
    void pushdown(int rt){
        if(turn[rt]){
            if(ls[rt]) down_turn(ls[rt]);
            if(rs[rt]) down_turn(rs[rt]);
            turn[rt]^=1;
        }
    }

至于split与merge中对pushdown的运用在代码中我会详细介绍

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+1e2;
const int inf=0x3f3f3f3f;
int n,m;
struct Tree{
	int ls[N],rs[N],val[N],rk[N],sz[N],turn[N],root,tot;
	
	void pushup(int rt){sz[rt]=sz[ls[rt]]+sz[rs[rt]]+1;}
	
    void down_turn(int rt){
        swap(ls[rt],rs[rt]);
        turn[rt]^=1;
    }
    
    void pushdown(int rt){
        if(turn[rt]){
            if(ls[rt]) down_turn(ls[rt]);
            if(rs[rt]) down_turn(rs[rt]);
            turn[rt]^=1;
        }
    }
	
	int getnew(int v){
		val[++tot]=v,sz[tot]=1,rk[tot]=rand();
		return tot;
	}
	
	void split(int rt,int v,int &x,int &y){
		if(!rt) {x=y=0;return ;}
		pushdown(rt);//在分裂自己前,务必要解决自己身上的标记 
		if(sz[ls[rt]]<v){//sz[rt]<=v,size要慎用,因为没有还未pushup 
			x=rt;
			split(rs[rt],v-sz[ls[rt]]-1/*v-sz[rt]*/,rs[rt],y);
		}
		else{
			y=rt;
			split(ls[rt],v,x,ls[rt]);
		}
		pushup(rt);
	}
	
	int merge(int x,int y){
		if(x) pushdown(x);// 在合并自己前,务必要解决自己身上的标记 
		if(y) pushdown(y);// 在合并自己前,务必要解决自己身上的标记 
		if(!x||!y){return x|y;}
		if(rk[x]<rk[y]){
			rs[x]=merge(rs[x],y);
			pushup(x);
			return x;
		}
		else{
			ls[y]=merge(x,ls[y]);
			pushup(y);
			return y;
		}
	}
	
	void insert(int v){
		int x,y;
		split(root,v,x,y);
		root=merge(merge(x,getnew(v)),y);
	}
	
	void flip(int l,int r){
		int x,y,z;
		split(root,r,y,z);
		split(y,l-1,x,y);
		swap(ls[y],rs[y]); 
		turn[y]^=1;
		root=merge(merge(x,y),z);
	}
	
	void __count(int rt){
		if(!rt) return ;
		pushdown(rt);
		__count(ls[rt]);
		printf("%d ",val[rt]);
		__count(rs[rt]);
	}
	
	void _count(){
		__count(root);
	}
}tree;
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) tree.insert(i);
    for(int i=1,l,r;i<=m;i++){
    	scanf("%d%d",&l,&r);
    	tree.flip(l,r);
	}
	tree._count();
}

        其他区间操作:

        除了区间翻转,fhq平衡树还可以处理区间赋值,区间最值;

        有些题比较毒瘤,我不多说,这里用一道例题结束这篇blog

        CF702F T-Shirts

        题面:

        有 n1<=n<=2*10^{5}) 种 T 恤,每种有价格 c_{i}​ 和品质 q_{i}

        有 m(1<=m<=2*10^{5}) 个客户要买 T 恤,第 i 个人有 v_{i} 元,每人每次都会买一件能买得起的 q_{i} 最大的 T 恤。一个人只能买一种 T 恤一件,所有人之间都是独立的。

        问最后每个人买了多少件 T 恤?如果有多个 q_{i}​ 最大的 T 恤,会从价格低的开始买。

Examples

Input

3
7 5
3 5
4 3
2
13 14

Output

2 3 

  Input

2
100 500
50 499
4
50 200 150 100

Output

1 2 2 1 

        思路:

                考虑将衣服 按质量排序,优先选择质量高的,其次为价格低的,对每一件衣服计算贡             献。 建立平衡树 维护客户剩余的钱 客户买的衣服数量  。

               但是,人手中的钱数减去 c_{i}​ 后可能会导致重复,即不能保证平衡树二叉搜索树的性              质,也无法完成 FHQ 的合并操作(合并两个 FHQ Treap 需要保证一个子树中的最大值小               于另一个子树中的最小值),怎么办呢?   

                方法是,对于有重复的部分,一个一个地暴力插入;不重复的部分,打标记即可。

        直觉地认为这肯定会超时,但是如果一个人手中的钱数(不妨设为 x )需要被暴力插入,其          需要满足: c_{i}<=x<2*c_{i}  (即:c_{i}>\frac{x}{2}

                观察到每次暴力插入的话人手中的钱数是会减去 c_{i} 的,因为 c_{i}大于 x 的一半,所以         每次暴力插入时都使 x 至少减少了一半,所以对于一个有 q_{i}​ 块钱的人来说其最多会被暴力         插入 O(log_{2} q_{i}) 次,所以可以保证总的时间复杂度为 O((n+\sum log_{2} q_{i})log_{2}n),在四秒的时         限内可以通过此题。

        code:

        

#include<bits/stdc++.h> 
using namespace std;
const int N=4e5+1e2;
const int inf=0x3f3f3f3f;
int n,m;
struct st{
	int c,q;
}co[N];
bool cmp(st x,st y){
	if(x.q==y.q) return x.c<y.c;
	return x.q>y.q;
}
struct Tree{
	int ls[N],rs[N],val[N],rk[N],sum[N],ly1[N],ly2[N],root,tot,top,s[N];
	
	void down1(int rt,int cc){
		if(rt) val[rt]-=cc;ly1[rt]+=cc;
	} 

	void down2(int rt,int cc){
		if(rt) sum[rt]+=cc;ly2[rt]+=cc;
	}

	void pushdown(int rt){
		if(ly2[rt]){
			if(ls[rt]) down2(ls[rt],ly2[rt]);
			if(rs[rt]) down2(rs[rt],ly2[rt]);
			ly2[rt]=0;
		}	
		if(ly1[rt]){
			if(ls[rt]) down1(ls[rt],ly1[rt]);
			if(rs[rt]) down1(rs[rt],ly1[rt]);
			ly1[rt]=0;
		}
	}

	void split(int rt,int v,int &x,int &y){
		if(!rt) { x=y=0; return ;}
		pushdown(rt);
		if(val[rt]<=v){
			x=rt;
			split(rs[x],v,rs[x],y);
		}else{
			y=rt;
			split(ls[y],v,x,ls[y]);
		}
	}

	int merge(int x,int y){
		if(x) pushdown(x);
		if(y) pushdown(y);
		if(!x||!y) return x|y;
		if(rk[x]<rk[y]){
			rs[x]=merge(rs[x],y);
			return x;
		}else{
			ls[y]=merge(x,ls[y]);
			return y;
		}
	}

	void insert(int v,int i){
		int x,y;
		val[i]=v,rk[i]=rand();
		split(root,v,x,y);
		root=merge(merge(x,i),y);
	}
	
	int get(int k){
		return sum[k];
	}
	
	void dfs(int rt){
		if(!rt) return ;
		pushdown(rt);
		dfs(ls[rt]),dfs(rs[rt]);
		s[++top]=rt,ls[rt]=rs[rt]=0;
	}
	
	void work(int c){
		int x,y,z;
		split(root,c-1,x,y);
		down1(y,c),down2(y,1);
		split(y,c-1,y,z);
		top=0;dfs(y);
		for(int i=1;i<=top;i++)
			split(x,val[s[i]],x,y),x=merge(merge(x,s[i]),y);
		root=merge(x,z);
	}
	
}tree;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d%d",&co[i].c,&co[i].q);
	sort(co+1,co+1+n,cmp);
	scanf("%d",&m);
	for(int i=1,v;i<=m;i++){
		scanf("%d",&v);
		tree.insert(v,i);
	}
	for(int i=1;i<=n;i++) {
		tree.work(co[i].c);
	}
	tree.dfs(tree.root);
	for(int i=1;i<=m;i++)
		printf("%d " ,tree.get(i));
} 

纵有风骨

posted @   MegaSam  阅读(64)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示