來自《算法競賽入門經典訓練指南》
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;
}