天天看点

用Treap实现名次树1.Treap实现名次树2.LA5031

来自《算法竞赛入门经典训练指南》

1.Treap实现名次树

1.简单介绍

Treap是一棵拥有键值和优先级两种权值的树。对于键值而言,这棵树是二叉排序树。对于优先级而言,这棵树是堆,即在这棵树的任意子树中,根的优先级是最大的。 不难证明,如果每个结点的优先级事先给定且互不相等,整棵树的形态也就唯一确定了,和元素的插入顺序无关。在Treap的插入算法中,每个节点的优先级是随机确定的。 因此各个操作的时间复杂度也是随机的。幸运的是,可以证明插入、删除和查找的期望时间复杂度均为O(nlogn)。

2.Treap结点的定义

struct Node
{
    Node *ch[2];//左右子树
    int r;//优先级,数值越大,优先级越高
    int v;//值
    int s;//结点总数
    int cmp(int x) const{
        if(x==v) return -1;
        return x<v?0:1;
    }
};
           

3.旋转

分为左旋和右旋

//d=0表示左旋,d=1表示右旋
void rotate(Node* &o,int d)
{
    Node* k=o->ch[d^1];
    o->ch[d^1]=k->ch[d];
    k->ch[d]=o;
    o=k;
}
           

注意小技巧:因为d=0或者1,d^1定价于1-d,但计算速度更快。关于左旋或者右旋,可自行百度……

4.插入结点

插入结点时,先随机给新结点一个优先级,然后执行普通的插入算法(根据键值大小判断插入哪颗子树中。执行完毕后,用左右旋,维护Treap树的堆性质。 如要采用递归实现,则只需在递归插入之后判断是否需要旋转。

//在以o为根的子树中插入键值x,修改o
void insert(Node* &o,int x)
{
    if(o==NULL){
        o=new Node();
        o->ch[0]=o->ch[1]=NULL;
        o->v=x;
        o->r=rand();
    }
    else{
        int d=o->cmp(x);
        insert(o->ch[d],x);
        if(o->ch[d]->r>o->r){
            rotate(o,d^1);
        }
    }
}
           

5.删除结点

删除结点,如果只有一颗子树,直接用这棵子树代替待删除结点成为根即可(注意o是叶子的情况也符合在内)。麻烦的是o有两棵子树的情况,我们先把优先级较高的一颗旋转到根,然后递归在另一棵子树中删除结点o。例如,左子结点的优先级较高,就必须进行右旋,否则会违反堆性质。

void remove(Node* &o,int x)
{
    int d=o->cmp(x);
    if(d==-1){
        if(o->ch[0]==NULL) o=o->ch[1];
        else if(o->ch[1]==NULL) o=o->ch[0];
        else{
            int d2=(o->ch[0]->r>o->ch[1]->r?1:0);
            rotate(o,d2);
            remove(o->ch[d2],x);
        }
    }
    else{
        remove(o->ch[d],x);
    }
}
           

6.查找函数

上面的代码没有处理“待插入值存在”和“待删除值不存在”这两种情况,因此在调用响应函数前,请进行一次查找。

int find(Node* o,int x)
{
    while(o!=NULL){
        int d=o->cmp(x);
        if(d==-1) return 1;//存在
        else o=o->ch[d];
    }
    return 0;//不存在
}
           

7.名次树

在利用Treap实现名次树中,每个结点均有一个附加域size,表示以它为根的子树的结点的总数。 名次树可以实现如下两种操作: Kth(k):找出第k小元素 Rank(x):值x的名次,即比x小的结点个数加1. 用Treap实现名次树的方法是这样的:新增一个size域,然后编写一个maintain函数,方法如下:

void maintain()
{
    s=1;
    if(ch[0]!=NULL) s+=ch[0]->s;
    if(ch[1]!=NULL) s+=ch[1]->s;
}
           

旋转操作也要修改,当s可能发生改变时,要调用maintain函数重新计算

//d=0表示左旋,d=1表示右旋
void rotate(Node* &o,int d)
{
    Node* k=o->ch[d^1];
    o->ch[d^1]=k->ch[d];
    k->ch[d]=o;
    //注意先维护o,再维护k
    o->maintain();
    k->maintain();
    o=k;
}
           

具体的模板,见下面一道例题。

2.LA5031

1.题目原文

https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=3032

有一张n个顶点m条边的无向图,每个结点有一个整数权值。任务是执行一系列操作,操作分三种。 1.D X:删除ID为X的边,保证每条边至多删一次 2.Q X k:计算与结点X连通的结点中(包括X本身),第k大的权值,如果不存在,返回0. 3.C X V:把结点X的权值改为V。 操作序列结束的标志是E。

2.解题思路

本题只需要设计离线算法,因此可以把操作顺序反过来处理。先读入所有操作,执行所有的操作D时,按照逆序把这些边逐步插入到图中。用一棵名次树维护一个连通分量中的点权。C操作对应修改操作,Q操作对应kth操作,而执行D操作,如果两个端点在一个连通分量中,显然无影响。否则,应该像并查集合并的操作那样,“权值”较小的并到“权值”较大的根节点中,这里的权值是结点数,这样的策略是启发式合并。采用启发式合并,可以高效处理(想一下并查集),对于任意结点来说,每当它被移动到新树中时,该结点所在树的大小至少加倍,任意结点至多“被移动”logn次,每次移动需要O(logn)时间,因此总时间复杂度为O(nlog^2n)。

3.AC代码

#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <set>
#include <vector>
#include<cmath>
#include<bitset>
#include<sstream>
using namespace std;
#define INF 0x7fffffff

struct Node
{
    Node *ch[2];//左右子树
    int r;//随机优先级
    int v;//值
    int s;//结点总数
    Node(int v):v(v){
        ch[0]=ch[1]=NULL;
        r=rand();
        s=1;
    }

    int cmp(int x) const{
        if(x==v) return -1;
        return x<v?0:1;
    }
    void maintain(){
        s=1;
        if(ch[0]!=NULL) s+=ch[0]->s;
        if(ch[1]!=NULL) s+=ch[1]->s;
    }
};

//d=0代表左旋,d=1代表右旋
void rotate(Node* &o,int d)
{
    Node *k=o->ch[d^1];
    o->ch[d^1]=k->ch[d];
    k->ch[d]=o;
    o->maintain();
    k->maintain();
    o=k;
}

void insert(Node* &o,int x)
{
    if(o==NULL) o=new Node(x);else{
        int d=(x<o->v?0:1);//不要用cmp函数,可能有相同结点
        insert(o->ch[d],x);
        if(o->ch[d]->r>o->r) rotate(o,d^1);
    }
    o->maintain();
}

void remove(Node* &o,int x)
{
    int d=o->cmp(x);
    if(d==-1){
        Node *u=o;
        if(o->ch[0]!=NULL&&o->ch[1]!=NULL){
            int d2=(o->ch[0]->r > o->ch[1]->r?1:0);
            rotate(o,d2);
            remove(o->ch[d2],x);
        }
        else{
            if(o->ch[0]==NULL) o=o->ch[1];
            else o=o->ch[0];
            delete u;
        }
    }
    else{
        remove(o->ch[d],x);
    }
    if(o!=NULL) o->maintain();

}

const int maxc=500000+10;
struct Command
{
    char type;
    int x,p;//根据type,p代表k或者v
}command[maxc];

const int maxn=20000+10;
const int maxm=60000+10;

int n,m;
int weight[maxn];
int from[maxm],to[maxm],removed[maxm];

//并查集相关
int pa[maxn];
int findset(int x)
{
    return pa[x]!=x?pa[x]=findset(pa[x]):x;
}

//名次树相关
Node *root[maxn];

//第k大值
int kth(Node* o,int k)
{
    if(o==NULL||k<=0||k>o->s) return 0;
    int s=(o->ch[1]==NULL?0:o->ch[1]->s);
    if(k==s+1) return o->v;
    else if(k<=s) return kth(o->ch[1],k);
    else return kth(o->ch[0],k-s-1);
}


void mergeto(Node* &src,Node* &dest)
{
    if(src->ch[0]!=NULL) mergeto(src->ch[0],dest);
    if(src->ch[1]!=NULL) mergeto(src->ch[1],dest);
    insert(dest,src->v);
    delete src;
    src=NULL;
}

void removetree(Node* &x)
{
    if(x->ch[0]!=NULL) removetree(x->ch[0]);
    if(x->ch[1]!=NULL) removetree(x->ch[1]);
    delete x;
    x=NULL;
}

//主程序相关
void add_edge(int x)
{
    int u=findset(from[x]),v=findset(to[x]);
    if(u!=v){
        if(root[u]->s<root[v]->s){
            pa[u]=v;
            mergeto(root[u],root[v]);
        }
        else{
            pa[v]=u;
            mergeto(root[v],root[u]);
        }
    }
}

int query_cnt;
long long query_tot;

void query(int x,int k)
{
    query_cnt++;
    query_tot+=kth(root[findset(x)],k);
}

void change_weight(int x,int v)
{
    int u=findset(x);
    remove(root[u],weight[x]);
    insert(root[u],v);
    weight[x]=v;
}

int main()
{
    int kase=0;
    while(scanf("%d%d",&n,&m)==2&&n){
        for(int i=1;i<=n;i++) scanf("%d",&weight[i]);
        for(int i=1;i<=m;i++) scanf("%d%d",&from[i],&to[i]);
        memset(removed,0,sizeof(removed));

        //读命令
        int c=0;
        for(;;){
            char type;
            int x,p=0,v=0;
            scanf(" %c",&type);
            if(type=='E') break;
            scanf("%d",&x);
            if(type=='D') removed[x]=1;
            if(type=='Q') scanf("%d",&p);
            if(type=='C'){
                scanf("%d",&v);
                p=weight[x];
                weight[x]=v;
            }
            command[c++]=(Command){type,x,p};
        }

        //最终的图
        for(int i=1;i<=n;i++){
            pa[i]=i;
            if(root[i]!=NULL) removetree(root[i]);
            root[i]=new Node(weight[i]);
        }

        for(int i=1;i<=m;i++){
            if(!removed[i]) add_edge(i);
        }

        //反向操作
        query_cnt=query_tot=0;
        for(int i=c-1;i>=0;i--){
            if(command[i].type=='D') add_edge(command[i].x);
            if(command[i].type=='C') change_weight(command[i].x,command[i].p);
            if(command[i].type=='Q') query(command[i].x,command[i].p);
        }

        printf("Case %d: %.6lf\n",++kase,query_tot/(double)query_cnt);
    }
    return 0;
}
           

继续阅读