FHQ-Treap(非旋treap/平衡树)——从入门到入坟

这篇具有很好参考价值的文章主要介绍了FHQ-Treap(非旋treap/平衡树)——从入门到入坟。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

       作者:hsez_yyh

       链接: FHQ-Treap——从入门到入坟_hsez_yyh的博客-CSDN博客

       来源:湖北省黄石二中信息竞赛组
       著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

        平衡树有很多种,其中fhq-treap 可以说是最强的一种平衡树之一,它可以维护值域,也可以维护下标,还能维护区间修改,更难能可贵的是,它可以完成splay都完成不了的可持久化操作。其唯一弱于splay的就是在LCT上,splay维护LCT的复杂度是O(nlog(n)),而fhq-treap的复杂度为O(nlog^2(n)),稍微大了一点,但是比splay好写多了。

        其实FHQ-treap的实现非常简单,本人认为,数据结构其实都不难实现,只是难于在概念上抽象的理解,所以,对于学习数据结构的最好方法就是手动模拟,在开始我们愉快的FHQ-treap之旅之前,在此先瞻仰一下FHQ-treap的最先引入者范浩强大佬。

        现在我们正式开始(其实迫于教练压力才写的这篇文章)

        首先fhq-treap是一个二叉搜索树(BST),它的每个节点有两个主要信息:keyval,key是我们fhq-treap要维护的键值,而val是随机生成的(rand()),key信息主要用于我们对于题目信息的处理,而val则是用于维持fhq-treap在结构上满足期望高度为logn的,val保证了fhq-treap拥有稳定的logn的高度,不会像splay一样退化。fhq-treap 除了要满足关于key的BST性质之外,还需满足关于val的小根堆性质。就是说,对于任意fhq-treap中的节点 i ,其左子树上的所有节点的key小于等于 i 节点的key值,i 节点所有右子树上所有节点的key 大于等于 i 节点的key值;对于任意节点 i ,其左、右儿子的val值大于等于 i 的val值(满足小根堆的性质)。要牢牢记住这两点的区别,否则会像我一样搞混,然后写代码的时候狂 WA,(手动悲剧)。下面给出一颗fhq-treap:

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

        上图就是一颗fhq-treap,其中每个节点中的数字为val,每个节点下方的数字为当前节点的key       可以发现一颗fhq-treap中序遍历后得到的序列是单调递增(如果维护方式不同其也可以是单调递减的)的。 非常好的性质——神犇ljz说。 这里,我们要引入fhq-treap的两个基本操作:splitmerge, 其中split是分离操作,merge是合并操作,下面我们来一一阐明:

        1. split  通常情况下,split用于分离一颗fhq-treap,对于一个元素 x ,我们会将这棵fhq-treap分裂为左右两棵树,左树上每个节点的key值都小于等于(<=)x , 而右树上的所有节点的key值都大于 x ;下面给出代码:

void split(int pos,int xx,int &l,int &r)// pos是当前fhq-treap的根
{  // &l 和 &r分别是fhq-treap分裂后得到的两个fhq-treap的根,这里用指针(引用)的方式,便于传递值
	if(!pos)//如果该节点为空树,则分裂成的左右两个树也应为空
	{
		l=r=0;
		return ;
	}
	if(tr[pos].key<=xx)// 如果当前节点的key值小于等于xx,则证明该节点的左子树上所有的key值也小于等于xx
	{
		l=pos;//让该节点成为左树的节点
		split(tr[l].r,xx,tr[l].r,r);//继续递归分离当前节点的右子树
	}
	else//同上
	{
		r=pos;
		split(tr[r].l,xx,l,tr[r].l);//继续递归分离当前节点的左子树
	}
	push_up(pos);
    //一定要记得push_up,因为pos点的左右子树大小发生了改变,否则会导致信息不对
}
//以上便是递归分离左右子树的方法

         以上代码如果学过线段树的话会好理解的多,本蒟蒻也建议先去熟练掌握线段树再来学习fhq-treap,这样会轻松很多。下面我们来模拟这个过程——本蒟蒻就是开始没有自己去模拟,导致一直不是很理解fhq-treap的实现方式。

        对于上图,假如我们要将其分离为小于等于18和大于18两个部分:

        fhq treap,冲击NOI,算法分析,数据结构,算法,c++

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

   

  在此之后,我们会继续递归,分裂K节点的左子树,大家可以自己手动模拟下(主要是画图太麻烦了,QAQ),由于I节点的权值20>18,所以我们将 I 节点归入右fhq-treap,接下来我们会碰到空节点,递归将会跳出,这时,我们就能得到左右两棵fhq-treap了:

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

以上就是fhq-treap的基本操作之一split操作的全部过程了 。

        2. merge 操作  这是一个合并操作,但它也不能随便合并,如果要合并两棵fhq-treap,那么它们可以进行合并当且仅当左fhq-treap上所有节点的key值都小于等于右fhq-treap中的最小key值(也就是左树小于等于右树)或两棵树中有空树。 这个性质很重要,为根据val合并两棵树做好的前提,保证了BST结构不会因合并而被破坏。 合并时,我们会根据左右两棵树的根节点的val值大小进行合并,大家先看一下代码:

int merge(int l,int r)//传入左右子树的根节点
{
	if(!l||!r) //如果有空树
	return l|r;//直接返回非空树(本蒟蒻这样的写法等同于l+r,是用来装杯的)
	if(tr[l].val<=tr[r].val)
	{
        //如果左树上当前节点的val值小于右树上当前节点的val值
        //将l变为合并后的新fhq-treap的根节点
		tr[l].r=merge(tr[l].r,r);//继续递归确定l的右子树该接谁
		push_up(l);//一定记得上传
		return l;//返回新得到的根节点
	}
	else// 同上
	{
		tr[r].l=merge(l,tr[r].l);
		push_up(r);
		return r;
	}
}

        还是一样,画图好理解:

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

以上是两棵要合并的fhq-treap

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

fhq treap,冲击NOI,算法分析,数据结构,算法,c++

 fhq treap,冲击NOI,算法分析,数据结构,算法,c++

 以上就是fhq-treap合并的全部过程了。

        到现在,fhq-treap的最基本也是精华所在你已经完全掌握了,下面我们来看一道题目:

【模板】普通平衡树 - 洛谷     这道题就是平衡树的板子题,用fhq-treap可以轻松切掉,代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,idx;
int dl,dr,temp,rt;
struct node
{
	int l,r;
	int key,val;
	int si;//以当前节点为根的树的节点数
}tr[N*2];
int get_rand(int xx)
{
	tr[++idx].key=xx;
	tr[idx].val=rand();
	tr[idx].si=1;
	return idx;
} //建立一个新节点
void push_up(int pos)
{
	tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
    // 注意这里,这里跟线段树的push_up不一样,要记得加上自己的大小
}
void split(int pos,int xx,int &l,int &r)//分离操作
{
	if(!pos)
	{
		l=r=0;
		return ;
	}
	if(tr[pos].key<=xx)
	{
		l=pos;
		split(tr[l].r,xx,tr[l].r,r);
	}
	else
	{
		r=pos;
		split(tr[r].l,xx,l,tr[r].l);
	}
	push_up(pos);
}
int merge(int l,int r) // 合并操作
{
	if(!l||!r)
	return l|r;
	if(tr[l].val<=tr[r].val)
	{
		tr[l].r=merge(tr[l].r,r);
		push_up(l);
		return l;
	}
	else
	{
		tr[r].l=merge(l,tr[r].l);
		push_up(r);
		return r;
	}
}
void insert(int xx)
{
    // 对于合并操作,先将原树分裂成<=xx-1和 >xx-1(>=xx)两个部分
    // 将xx与<=xx-1的部分合并,再和>xx-1的部分合并
	split(rt,xx-1,dl,dr);
	rt=merge(merge(dl,get_rand(xx)),dr);
}
void delet(int xx)
{
	split(rt,xx-1,dl,dr);
	split(dr,xx,temp,dr);
    //连续两次分裂,temp得到了一棵所有key值==xx的fhq-treap
	temp=merge(tr[temp].l,tr[temp].r);
    //由于只能删一个xx,而原树中可能有很多xx,那么我们直接合并temp的左右子树即可
	rt=merge(dl,merge(temp,dr));
    // 将原树恢复(已删去xx)
}
int get_rk(int xx)
{
//非常简单,直接分离出<=xx-1的一棵树,也就是<xx的一棵树
//然后xx的排名就是<xx的树的大小+1即可
	split(rt,xx-1,dl,dr);
	int rnk=tr[dl].si+1;
	rt=merge(dl,dr);
	return rnk;
}
int get_num(int pos,int xx)
{
//这里采用递归搜索,思想见二分搜索树
	int u=tr[tr[pos].l].si+1;
	if(u==xx)
	return tr[pos].key;
	if(u>xx) return get_num(tr[pos].l,xx);
	else return get_num(tr[pos].r,xx-u);
}
int main()
{
	scanf("%d",&n);
	int op,xx;
	while(n--)
	{
		scanf("%d %d",&op,&xx);
		if(op==1) insert(xx);
		else if(op==2) delet(xx);
		else if(op==3) printf("%d\n",get_rk(xx));
		else if(op==4) printf("%d\n",get_num(rt,xx));
		else if(op==5)
		{
			split(rt,xx-1,dl,dr);
			printf("%d\n",get_num(dl,tr[dl].si));
            // 前驱就是<=xx-1的这棵树中排名最后的那个数
			rt=merge(dl,dr);
		}
		else 
		{
			split(rt,xx,dl,dr);
			printf("%d\n",get_num(dr,1));
            //后继就是>xx的这棵树中排名第一的树    
			rt=merge(dl,dr);
		}
	}
	return 0;
}

        好了,目前为止,平衡树维护值域的方法你就已经掌握了,下面我们再来看一个fhq-treap维护序列的题目:

【模板】文艺平衡树 - 洛谷 这道题就是典型的fhq-treap维护序列,操作也非常简单,仅仅是加上了一个翻转的懒标记而已——可见学线段树的必要。

        值得一说的是,fhq-treap在维护序列时,void split(int pos,int k,int &l,int &r)的意思不再是分离<=k和>k的两棵树,而是变为了 将序列pos 的前k个元素分离出来形成一棵新fhq-treap l,另外的一部分形成另一颗 fhq-treap r .

         下面展示代码: 

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m;
struct node
{
	int l,r;
	int key,val;
	int si;
	bool tag;
}tr[N*2];
int dl,dr,temp,rt,idx;
int get_rand(int xx)
{
	tr[++idx].key=xx;
	tr[idx].val=rand();
	tr[idx].si=1;
	return idx;
}
void push_up(int pos)
{
	tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
}
void push_down(int pos)
{
    //下传懒标记,如果一直下传下去,将会实现左右区间翻转
	if(tr[pos].tag)
	{
		tr[tr[pos].l].tag^=1;
		tr[tr[pos].r].tag^=1;
		swap(tr[pos].l,tr[pos].r);
		tr[pos].tag=0;
	}
}
void split(int pos,int k,int &l,int &r)
{
	if(!pos)
	{
		l=r=0;
		return ;
	}
    //只要对某个节点的子节点做操作,就一定要将该节点的标记下传
	push_down(pos);
	int u=tr[tr[pos].l].si+1;
	if(u<=k)
	{
		l=pos;
		split(tr[pos].r,k-u,tr[pos].r,r);
		push_up(l);
	}
	else
	{
		r=pos;
		split(tr[pos].l,k,l,tr[pos].l);
		push_up(r);
	}
}
int merge(int l,int r)
{
	if(!l||!r) return l|r;
	push_down(l);
	push_down(r);
	if(tr[l].val<=tr[r].val)
	{
		tr[l].r=merge(tr[l].r,r);
		push_up(l);
		return l;
	}
	else
	{
		tr[r].l=merge(l,tr[r].l);
		push_up(r);
		return r;
	}
}
void reverse(int l,int r)
{
	int p1=0,p2=0,p3=0,p4=0;
	split(rt,l-1,p1,p2);
	split(p2,(r-l+1),p3,p4);
    // 将要翻转的一段分离出来,打上标记,再合并回去
	tr[p3].tag^=1;
	p2=merge(p3,p4);
	rt=merge(p1,p2);
}
void print_res(int pos)
{
	push_down(pos);
	if(tr[pos].l) print_res(tr[pos].l);
	printf("%d ",tr[pos].key);
	if(tr[pos].r) print_res(tr[pos].r);
    // 迭代输出,没什么好说的
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		split(rt,i-1,dl,dr);
		rt=merge(merge(dl,get_rand(i)),dr);
	}
	int xx,yy;
	while(m--)
	{
		scanf("%d %d",&xx,&yy);
		reverse(xx,yy);
	}
	print_res(rt);
	return 0;
}

        到此,平衡树最大的两个功能都能实现了 Orz ! ! !

        但既然要入坟,那么在此推荐两道题目: 

         266. 超级备忘录 - AcWing题库

         [NOI2005] 维护数列 - 洛谷                祝大家WA开心,本蒟蒻虽然过了,但是懒得贴代码了——都是泪

        你以为就这样结束了??? 不,还远远不止,既然要入坟,那么就要坚持到底:

fhq-treap之所以是现在OI界地位逐渐超过splay的结构,自然有splay做不到的地方。众所周知 ,splay依靠二次旋转来保持logn的复杂度的,然而,这样做会经常改变树的结构,自然,splay对于处理可持久化的信息是一点办法都没有——就是嘛,转来转去的一看就不好  。但是嘛,splay可以做的fhq-treap可以做,splay不能做的fhq-treap还可以做(就算lct复杂度高一点,但是也是可以做的嘛,手动滑稽) 。

        口胡完毕,进入正题: 细心的你可以发现,在fhq-treap的 split 操作和 merge 操作时,由于fhq-treap的期望高度是 logn,并且可以发现 split 和 merge 时都是自上而下的,那么,对于每次 split 和 merge 操作,真正有所变动的点只有 logn 个  ,所以我们只需要在操作过程中记录下沿途经过的 logn 个节点,就能对某次历史版本有所记录,其记录方式与主席树类似——又是线段树 。

如果再仔细思考,我们可以发现,每次 merge 操作都是合并先由 split 操作分离的两棵树,所以,对于真正结果有影响的操作只有 split ,所以,我们只需要在每次 split 的时候记录沿途的节点即可。

例题: 【模板】可持久化平衡树 - 洛谷

需要注意的是,我们每次操作产生的新版本,都要创建一个新根来指向次版本。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e5+10;
int n,dl,dr,temp;
struct node
{
	int l,r;
	LL key,val;
	LL si;
}tr[N*60];
int rt[N*60],idx;
void push_up(int pos)
{
	tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
 } 
int get_rand(int xx)
{
	tr[++idx].key=xx;
	tr[idx].val=rand();
	tr[idx].si=1;
	return idx;
}
void split(int pos,int xx,int &l,int &r)
{
	if(!pos)
	{
		l=r=0;
		return ;
	}
	if(tr[pos].key<=xx)
	{
		l=++idx;
		tr[l]=tr[pos];
		split(tr[l].r,xx,tr[l].r,r);
		push_up(l);
	}
	else
	{
		r=++idx;
		tr[r]=tr[pos];
		split(tr[pos].l,xx,l,tr[r].l);
		push_up(r);
	}
}
int merge(int l,int r)
{
	if(!l||!r) return l|r;
	int pos=++idx;
	if(tr[l].val<=tr[r].val)
	{
		tr[pos]=tr[l];
		tr[pos].r=merge(tr[pos].r,r);
	}
	else
	{
		tr[pos]=tr[r];
		tr[pos].l=merge(l,tr[pos].l);
	}
	push_up(pos);
	return pos;
}
void insert(int &pos,int xx)
{
	split(pos,xx-1,dl,dr);
	pos=merge(merge(dl,get_rand(xx)),dr);
}
void delet(int &pos,int xx)
{
	split(pos,xx-1,dl,dr);
	split(dr,xx,temp,dr);
	temp=merge(tr[temp].l,tr[temp].r);
	pos=merge(dl,merge(temp,dr));
}
int get_rk(int &pos,int xx)
{
	split(pos,xx-1,dl,dr);
	int rk=tr[dl].si+1;
	pos=merge(dl,dr);
	return rk;
}
int get_num(int pos,int xx)
{
	int u=tr[tr[pos].l].si+1;
	if(u==xx) return tr[pos].key;
	if(u>xx) return get_num(tr[pos].l,xx);
	else return get_num(tr[pos].r,xx-u);
}
int main()
{
	scanf("%d",&n);
	int op,xx,t;
	int cnt=0;
	while(n--)
	{
		cnt++;
		scanf("%d %d %d",&t,&op,&xx);
		rt[cnt]=rt[t];
		if(op==1) insert(rt[cnt],xx);
		else if(op==2) delet(rt[cnt],xx);
		else if(op==3) printf("%d\n",get_rk(rt[cnt],xx));
		else if(op==4) printf("%d\n",get_num(rt[cnt],xx));
		else if(op==5)
		{
			split(rt[cnt],xx-1,dl,dr);
			printf("%d\n",get_num(dl,tr[dl].si));
			rt[cnt]=merge(dl,dr);
		}
		else
		{
			split(rt[cnt],xx,dl,dr);
			printf("%d\n",get_num(dr,1));
			rt[cnt]=merge(dl,dr);
		}
	}
	return 0;
}

        既然有了这道题,那么你肯定能想到另外一道:【模板】可持久化文艺平衡树 - 洛谷

其实和普通的文艺平衡树一样,只需要多加一个可持久化操作即可,代码如下(也就写了一小时):

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+10;
int n,idx;
int dl,dr,temp;
struct node
{
	int l,r;
	int key,val;
	int si;
	bool rev;
	LL sum;
}tr[N*100];
int rt[N];
void push_up(int pos)
{
	tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
	tr[pos].sum=tr[tr[pos].l].sum+tr[tr[pos].r].sum+tr[pos].key;
}
int get_rand(int xx)
{
	tr[++idx].sum=xx;
	tr[idx].key=xx;
	tr[idx].si=1;
	tr[idx].val=rand();
	return idx;
}
void push_down(int &pos)
{
	if(!tr[pos].rev) return ;
	if(tr[pos].l)
	{
		tr[++idx]=tr[tr[pos].l];
		tr[pos].l=idx;
		tr[tr[pos].l].rev^=1;
	}
	if(tr[pos].r)
	{
		tr[++idx]=tr[tr[pos].r];
		tr[pos].r=idx;
		tr[tr[pos].r].rev^=1;
	}
	tr[pos].rev=0;
	swap(tr[pos].l,tr[pos].r);
}
void split(int pos,int k,int &l,int &r)
{
	if(!pos)
	{
		l=r=0;
		return ;
	}
	push_down(pos);
	int u=tr[tr[pos].l].si+1;
	if(u<=k)
	{
		l=++idx;
		tr[l]=tr[pos];
		split(tr[pos].r,k-u,tr[l].r,r);
		push_up(l);
	}
	else
	{
		r=++idx;
		tr[r]=tr[pos];
		split(tr[pos].l,k,l,tr[r].l);
		push_up(r);
	}
}
int merge(int l,int r)
{
	if(!l||!r) return l|r;
	int pos=++idx;
	if(tr[l].val<=tr[r].val)
	{
		tr[pos]=tr[l];
		push_down(pos);
		tr[pos].r=merge(tr[pos].r,r);
		push_up(pos);
		return pos;
	}
	else
	{
		tr[pos]=tr[r];
		push_down(pos);
		tr[pos].l=merge(l,tr[pos].l);
		push_up(pos);
		return pos;
	}
}
void insert(int &pos,int xx,int yy)
{
	split(pos,xx,dl,dr);
	dl=merge(dl,get_rand(yy));
	pos=merge(dl,dr);
}
void delet(int &pos,int xx)
{
	split(pos,xx-1,dl,dr);
	split(dr,1,temp,dr);
	pos=merge(dl,dr);
}
void reverse(int &pos,int l,int r)
{
	split(pos,l-1,dl,dr);
    split(dr,r-l+1,temp,dr);
    tr[temp].rev^=1;
    dr=merge(temp,dr);
    pos=merge(dl,dr);
}
LL query(int &pos,int l,int r)
{
	split(pos,l-1,dl,dr);
    split(dr,r-l+1,temp,dr);
    LL ans=tr[temp].sum;
    dr=merge(temp,dr);
    pos=merge(dl,dr);
    return ans;
}
int main()
{
	scanf("%d",&n);
	int op,v,xx,yy;
	LL lan=0;
	for(int t=1;t<=n;t++)
	{
		scanf("%d %d",&v,&op);
		rt[t]=rt[v];
		if(op==2)
		{
			scanf("%d",&xx);
			xx^=lan;
			delet(rt[t],xx);
		}
		else
		{
			scanf("%d %d",&xx,&yy);
			if(op==1)
			{
				xx^=lan,yy^=lan;
				insert(rt[t],xx,yy);
			}
			else if(op==3)
			{
				xx^=lan,yy^=lan;
				reverse(rt[t],xx,yy);
			}
			else
			{
				xx^=lan,yy^=lan;
				lan=query(rt[t],xx,yy);
				printf("%lld\n",lan);
			}
		}
	}
	return 0;
}

        一定要注意的是,每次操作,不管是合并、分离还是push_down,只要影响到树形和树中节点的信息,就必须新建节点来储存,所以fhq-treap的可持久化格外的费空间,不过现在空间都给的足,所以一般都能过。用fhq-Treap支持区间翻转和可持久化,需要注意的是和区间修改主席树一样,在push_down翻转rev的时候需要复制左右儿子结点,把rev推到新复制的结点上。

        多体会下,就能很容易理解啦,在本蒟蒻看来,其实平衡树比线段树简单多了 qwq

        好了,现在已经快入坟了,就差轻轻一推了,那么它来啦!!! 作为平衡树五题的压轴题目,也是平衡树的另一大考点:【模板】二逼平衡树(树套树) - 洛谷         相信有人已经看不下去了,本蒟蒻也是到这里才体会到了平衡树的快乐 ,当本蒟蒻听到树套树之后,直接兴奋的半个小时交了一发,然后快乐的只过了一个点。在听完课后,虽然本蒟蒻会写了,但是,但是!,又交了一发,只过了90分(关键是就我第一次提交过的那个点没过——离离原上谱) ,虽然后来我看题解用树状数组套平衡树过了,However,这和我线段树套平衡树的初衷不相符,所以这里就不贴代码,就稍微解释一下吧(尬)。

        对于此题,我们可以发现,如果单纯的求前驱和后继,排名,数值,用一棵fhq-treap来维护绰绰有余,然而,此题异常毒瘤,它让我们在一个区间中进行上述操作。区间? 这不又是线段树嘛,欸,猜对了。又题目的提示可以知道:我们要在一棵维护区间的线段树上做平衡树,也就是下标线段树套值域平衡树,线段树的每个节点都是一段区间,所以我们要在线段树的每个节点上开一个平衡树,这样就可以轻松地维护信息啦——但是,这样也会有问题,我们在查询排名为k的数时,由于答案需要多棵不可合并的平衡树一起提供,所以我们要在线段树二分区间(logn)的前提下在平衡树中二分答案(logn*logn),那么复杂度就会到达 log^3(n),这样又会被出题人卡死。怎么办呢,为了降低复杂度,我们可以利用线段树的二分性,把树套树改成 下标平衡树套值域线段树,这样复杂度就会将下来,具体实现推荐去看肖然大佬的blog 【树套树】 - 肖然 的博客 - 洛谷博客

        本蒟蒻就水到这了,反正我是已经入坟躺平了,希望各位神犇大佬AC顺利 Orz文章来源地址https://www.toymoban.com/news/detail-604978.html

到了这里,关于FHQ-Treap(非旋treap/平衡树)——从入门到入坟的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C#从入门到入坟(不易,转载请注明出处)

    安装Visual Studio。 下载地址:https://visualstudio.microsoft.com/zh-hans/ 可以选择社区版本,是可以免费使用的。 下载之后配置安装。 按照自己的工作需要,勾选相应的组件和安装位置,进行安装即可。 目前C#开发的两种框架 运行于windows的.Net Framework 可以跨平台的.Net6 项目名称 建议

    2024年02月05日
    浏览(29)
  • BST-Treap名次树指针实现板子 Ver2.1

    为了更好的阅读体验,请点击这里 这里只有板子没有原理QWQ 可实现 1.插入 x 数 2.删除 x 数(若有多个相同的数,只删除一个) 3.查询 x 数的排名(排名定义为比当前数小的数的个数 +1) 4.查询排名为 x 的数 5.求 x 的前驱(前驱定义为小于 x,且最大的数) 6.求 x 的后继(后继定义为大

    2024年02月08日
    浏览(24)
  • Atcoder Beginner Contest 324 G Generate Arrays 题解-Treap

    为了更好的阅读体验,请点击这里 题目链接 套上平衡树板子就能做的很快的题,然后因为是指针存树,因此交换只需要把序列大小较小的挨个拿出来插到相应的地方即可。复杂度 (O(N log^2 N)) 。 但是一定要记住 不可以直接使用 std::swap 交换包含带有指针的类的实例(如代码

    2024年02月08日
    浏览(31)
  • 驱动开发——入门到入职1

    字符设备驱动:按照字节流来访问,只能顺序访问,不能无序访问的设备 块设备驱动:按照block(512字节)访问,可以随机访问的设备。 网络设备驱动:网络设备没有设备节点,控制网卡硬件,负责网络数据收发的代码就是网络设备驱动 入口:资源申请,在安装驱动的时候执行

    2024年01月24日
    浏览(33)
  • 《小程序从入门到入坑》框架语法

    哈喽大家好,我是 SuperYing ,我们继续 小程序入门系列 ,本文将对小程序框架语法进行比较全面的介绍。在 《小程序从入门到入坑》简介及工程创建 中,我们提到小程序项目结构,主要包括 app.json , app.js , app.wxss 以及页面(组件)级的 .wxml , .wxss , .js , .json 。接下来我

    2024年03月27日
    浏览(28)
  • Rust语言从入门到入坑——(5)Rust 所有权

    主要介绍Rust所有权的知识,涉及到变量的作用域,内存释放机制,移动,克隆,引用等知识,很多知识是Rust语言特有机制。 所有权有以下三条规则: - Rust 中的每个值都有一个变量,称为其所有者。 - 一次只能有一个所有者。 - 当所有者不在程序运行范围时,该值将被删除

    2024年02月10日
    浏览(34)
  • Rust语言从入门到入坑——(2)Rust在windows上搭建开发环境

    开始搭建一个适合在windows上运行的Rust环境。 Rust支持的程序语言很多:可详见官网介绍 本文章主要是在windowns下搭建开发环境 首先,需要安装最新版的 Rust 编译工具和 Visual Studio Code。 Rust 编译工具:https://www.rust-lang.org/zh-CN/tools/install Visual Studio Code:https://code.visualstudio.com

    2024年02月09日
    浏览(35)
  • 复刻stm32平衡小车(适合入门)

    目录 前言: 1.硬件部分 1.1 STM32最小系统 1.2 电源 1.3 TB6612电机驱动模块 1.4 串口通信 1.5 OLED模块 1.6 蓝牙模块 1.7 LED灯模块  1.8 MPU6050模块​编辑 1.9 硬件焊接与调试 1.10 组装 2.软件部分 2.1 代码 2.2 逻辑实现 2.2.1 control.c 2.2.2 usart.c 结尾 本文主要为复刻b站up主开源的平衡小车以及

    2024年02月13日
    浏览(39)
  • Kafka入门,分区的分配再平衡(二十)

    1、kafka有四种主流的分区策略:Range,RoundRobin,Sticky,CooperativeSticky。可以通过配置参数partition.assignment.strategy,修改分区的分配策略。默认策略是Ranage+CooperativeSticky。Kafka可以同事使用多个分区分配策略。 参数 描述 heartbeat.interval.ms Kafka消费者和coordinator之间的心跳时间,默认3s。

    2024年02月12日
    浏览(68)
  • 三天让车立起来!STM32平衡车入门PID —— 第三天(PID调参)

    说明:本文章适用于STM32初学者,想完成一个好玩且有深度的项目但不知道从何下手的同学。 PID调参是平衡车的精髓所在,参数整定的好坏直接影响到平衡车的平衡效果。有的车平衡时来回晃而参数选的好的车就能稳稳地平衡在原地。可见PID调参在PID中是重中之重。我们一定

    2023年04月18日
    浏览(26)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包