来自《算法竞赛入门经典训练指南》
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;
}