学习笔记-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函数提供不同的种子值,进而产生不同的随机数序列,参考博客


自己选择的路,再难也要走下去_(:з」∠)_

おすすめ

転載: blog.csdn.net/weixin_51443397/article/details/119253955