Treap树(21.7.30)
Posted 未定_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Treap树(21.7.30)相关的知识,希望对你有一定的参考价值。
一、Treap树
平衡二叉搜索树,Treap树是树和堆的结合,即每个结点有一个键值和一个被称为优先级的权值,树对于键值是排序二叉树,对于优先级是堆。(堆特征:根结点优先级最大)
二、操作
1.插入:把新结点node插入到Treap树
•把node按键值大小插入合适子树
•若node优先级比父结点高,node往上走,需要旋转。
有关旋转: 如果当前结点的优先级比根结点大就旋转,如果当前节点是根的左儿子就右旋,如果当前节点是根的右儿子就左旋。
以左旋为例:注意图中黄色标注。(右←左)
红色表示优先级,铅笔表示键值。易见旋转后仍为二叉搜索树。图中k的优先级高于父结点o,k为右儿子,所以左旋,k上升。k的左儿子x变成o的右儿子,o变成k的左儿子。
以下为旋转的代码,解释按左旋,右旋同理。
void rotate(Node* &o,int d)//d=0,左旋;d=1,右旋
{
Node *k=o->son[d^1];//d^1与1-d等价,k指针指向o的右儿子k,即指向要被操作的结点
o->son[d^1]=k->son[d];//k的左儿子x变成o的右儿子
k->son[d]=o;//o变成k的左儿子
o=k;//返回新的根k
}
右旋同理:
2.删除
方式1: 用二叉树搜索树的方式删除
•
叶结点(没有非空子结点的结点),直接把结点删除即可。
•链结点(只有一个非空子结点的结点),把它的子结点代替它的位置,然后把它删除。
•结点(有两个非空子结点)。用它右子树的最小值来代替它,然后把它删除。或者用结点的左子树的最大值来替代它。
方式2:用堆的方式删除
把要删除的结点旋转到叶结点上,然后直接删除即可。
即:如果待删结点的左子结点的优先级小于右子结点的优先级,右旋该结点,反之,左旋。
右旋注意图中6 4 5,左旋注意6 7(忽略铅笔痕迹,铅笔标注有误)
参考:一篇很好的博客
3.Treap与名次树问题
-名次树功能:
•找到第k大的元素
•查询元素x的名次
该功能的实现借助于给每个结点增加size值,该值为以它为根的子树的结点总数量。如下图:
例题:Shaolin
题意: 少林的和尚加入少林寺,每人有一个独立的id和独立的战斗等级,到寺之后与和他水平相近的老和尚战斗,方丈也就是第一个和尚的id=1,水平是1,000,000,000,已知id(按照入寺顺序)和战斗等级,问每个人跟谁战斗。
法1:map
把战斗等级映射到id上,新来一个人进队一个人,然后找等级接近的老和尚输出id即可。
#include<bits/stdc++.h>
using namespace std;
map<int,int>mp;//it->first是等级,it->second是id
int main()
{
int n;
while(~scanf("%d",&n)&&n)
{
mp.clear();
mp[1e9]=1;//方丈1,等级是1000000000
while(n--)
{
int id,g;
scanf("%d%d",&id,&g);
mp[g]=id;//新和尚进队
int ans;
map<int,int>::iterator it=mp.find(g);//找到排好序的位置
if(it==mp.begin())
ans=(++it)->second;
else
{
map<int,int>::iterator it2=it;
it2--,it++;//等级接近的前后两个老和尚
if(g- it2->first <= it->first -g)
ans=it2->second;
else ans=it->second;
}
printf("%d %d\\n",id,ans);
}
}
}
法2:Treap树
原理上跟map一样,进一个和尚就把该和尚放到Treap树里。
以下包含Treap树-名次树的基本操作,插入,旋转,返回第k大的树,返回元素k的名次,可作模板使用。
#include<bits/stdc++.h>
using namespace std;
int id[5000000+5];
struct Node
{
int size;//以这个结点为根的子树的结点总数量,用于名次树
int rank;//优先级
int key;//键值
Node * son[2];//son[0]是左儿子,son[1]是右儿子
bool operator < (const Node &a)const
{
return rank<a.rank;
}
int cmp(int x)const//判断x是当前键值key的左子树还是右子树
{
if(x==key)
return -1;
return x<key?0:1;
}
void update()//更新size值
{
size=1;
if(son[0]!=NULL)
size+=son[0]->size;
if(son[1]!=NULL)
size+=son[1]->size;
}
};
void rotate (Node* &o,int d)//d=0,左旋;d=1右旋
{
Node *k=o->son[d^1];//d^1与1-d等价,但更快
o->son[d^1]=k->son[d];
k->son[d]=o;
o->update();//更新size值
k->update();//更新size值
o=k;//返回新的根
}
void insert(Node* &o,int x)//把x插入到树中,x为键值
{
if(o==NULL)
{
o=new Node();
o->son[0]=o->son[1]=NULL;
o->rank=rand();//优先级随便设置一个
o->key=x;//赋值键值
o->size=1;//初始size只有1,左右子树都没有
}
else
{
int d=o->cmp(x);//判断键值x在当前o所指向键值的左边还是右边,0左1右,给d赋值
insert(o->son[d],x);// 继续循环
o->update();//更新size值
if(o<o->son[d])//父结点size值<儿子size值
rotate(o,d^1);//父结点旋转
}
}
int kth(Node* o,int k)//返回第k大的数
{
if(o==NULL||k<=0||k>o->size)
return -1;
int s=o->son[1]==NULL?0:o->son[1]->size;//s指向右儿子的size值
if(k==s+1)
return o->key;//如果k等于右儿子size值+1,则第k大的数为它本身
else if(k<=s)
return kth(o->son[1],k);//如果k<=右儿子的size值,说明第k大的数在右子树里,在右子树里继续循环
else return kth(o->son[0],k-s-1);//如果k>右儿子的size值+1,说明第k大的数在左子树里,在左子树里继续循环,k-s-1即为k-右儿子的size值-本身是1个
}
int find(Node* o,int k)//返回元素k的名次
{
if(o==NULL)
return -1;
int d=o->cmp(k);//判断键值k在当前o所指向键值的左边还是右边,0左1右,给d赋值
if(d==-1)//为o本身
return o->son[1]==NULL?1:o->son[1]->size+1;//返回本身的名次,即右儿子的size值+1
else if(d==1)//元素k在o的右面
return find(o->son[d],k);//在右儿子里继续找
else//元素k在o的左面
{
int tmp=find(o->son[d],k);//在左儿子里继续找,tmp为k在左子树里的名次
if(tmp==-1)
return -1;
else return o->son[1]==NULL?tmp+1:tmp+1+o->son[1]->size;//返回k在当前名次,即左子树里的名次加上o结点的名次
}
}
int main()
{
int n;
while(~scanf("%d",&n)&&n)
{
srand(time(NULL));//生成随机数
int k,g;
scanf("%d%d",&k,&g);//输入id和战斗等级
Node *root=new Node();//root指针指向新结点
root->son[0]=root->son[1]=NULL;//新结点左右儿子都为空
root->rank=rand();//新结点的优先级任意
root->key=g;//新结点的键值为战斗等级
root->size=1;
id[g]=k;//数组为了找到战斗等级对应的id
printf("%d %d\\n",k,1);//第一个和尚一定是和方丈战斗
for(int i=2;i<=n;i++)//从第二个开始
{
scanf("%d%d",&k,&g);
id[g]=k;
insert(root,g);//插入键值为g的结点到树中,也就是增加一个新和尚
int t=find(root,g);//返回新和尚的名次
int ans1,ans2,ans;
ans1=kth(root,t-1);//返回比新和尚小一个名次的人的战斗等级
ans2=kth(root,t+1);//返回比新和尚大一个名次的人的战斗等级
if(ans1!=-1&&ans2!=-1)
ans=ans1-g>=g-ans2?ans2:ans1;
else if(ans1==-1)
ans=ans2;
else ans=ans1;
printf("%d %d\\n",k,id[ans]);//id[ans]找到战斗等级对应的人的id
}
}
return 0;
}
补充:
1.rand()与srand()
•rand()函数返回一个从0到最大随机数的任意整数,最大随机数的大小通常是固定的一个大整数。
产生(a,n-1+a)的随机数,int num = rand() % n +a;
其中的a是起始值,n-1+a是终止值,n是整数的范围。(或者rand() % (b-a+1)+ a ;
就表示 a~b 之间的一个随机整数。)
•rand函数在产生随机数前,需要系统提供的生成伪随机数序列的种子,rand根据这个种子的值产生一系列随机数。如果系统提供的种子没有变化,每次调用rand函数生成的伪随机数序列都是一样的。srand(unsigned seed)通过参数seed改变系统提供的种子值,从而可以使得每次调用rand函数生成的伪随机数序列不同,从而实现真正意义上的“随机”。通常可以利用系统时间来改变系统的种子值,即srand(time(NULL)),可以为rand函数提供不同的种子值,进而产生不同的随机数序列,参考博客。
自己选择的路,再难也要走下去_(:з」∠)_
以上是关于Treap树(21.7.30)的主要内容,如果未能解决你的问题,请参考以下文章