二×排序樹(BST),的主要性質是有序性,樹的中序遍歷始終爲同一個有序序列,也就是對於每一節點u u的左兒子的鍵值應當小於u,右兒子鍵值大於u。
最佳狀態平衡樹高度爲log(N),這意味着插入,刪除,查詢,查詢排名等多種操作可以在log(N)的時間內完成(0.0)
但是在實際操作中,排序樹的高度難以控制,有時可以被卡成一條鏈(操作時間複雜度變成O(N)),這些待會再說
排序樹的基本操作
0.節點
struct Node {
int v, ch[2], fa, sz;
} d[MAXD];
int tot = 0;
inline int newNode() {
memset(d + ++tot, 0, sizeof d[0]);
return tot;
}
v: 鍵值
ch: 左右兒子,空爲0
fa: 父親
sz: 以當前節點爲根節點子樹大小,可用於排名查詢操作
1.旋轉 (旋轉,跳躍!)
旋轉是一種能改變節點位置而同時保持排序樹中序遍歷的有序性的操作。
比如要把X節點旋轉到它的父親Y所在位置,
如下圖所示
可見旋轉後樹的中序遍歷並沒有改變
代碼如下 : (把X向上旋轉)
inline void updata(int x) {
d[x].sz = d[d[x].ch[0]].sz + d[d[x].ch[1]].sz + 1;
}
inline void rotate(int x) {
int y = d[x].fa;
bool f = (d[y].ch[1] == x);
d[x].fa = d[y].fa;
d[y].fa = x;
if(d[x].fa) d[d[x].fa].ch[d[d[x].fa].ch[1] == y] = x;
d[y].ch[f] = d[x].ch[!f];
if(d[x].ch[!f]) d[d[x].ch[!f]].fa = y;
d[x].ch[!f] = y;
updata(y);
updata(x);
}
2.插入
從排序樹的根節點向下行走近似於二分查找的操作(由於樹的不完全平衡可能會略慢)
找到應該插入的位置進行插入,
最後向上進行必要的調整操作(依排序樹類型而定)
#1 按照鍵值插入
int getPos(int val) {
int u = root,l = 0;
while(u) {
if(d[u].v == val) return u;
l = u;
u = d[u].ch[val > d[u].v];
}
return l;
}
int insert(int val) {
int p = getPos(val);
if(p && d[p].v == val) {d[p].cnt ++; return p;}
int r = newNode();
if(p) d[p].ch[val > d[p].v] = r;
d[r].fa = p;
d[r].v = val;
d[r].cnt = 1;
d[r].pri = random(MAXP);
if(!root) root = r;
adjust(r);
return r;
}
#2 按照排名插入類似於按照鍵值, 尋找插入位置方法類似於下面的函數
int getByRank(int rk, int u = root) {
while(u) {
if(d[d[u].ch[0]].sz >= rk)
u = d[u].ch[0];
else if(d[d[u].ch[0]].sz + 1 >= rk)
return u;
else rk -= d[d[u].ch[0]].sz + 1, u = d[u].ch[1];
}
return u;
}
3.刪除
不同的排序樹有不同的刪除方法, 但他們都支持刪除, 具體見下面的每一種排序樹
ps : 通用簡單但是略暴力的方法是僞刪除,找到原節點後打個vis標記表示之後的操作忽略該節點即可
4.查找
從排序樹的根節點向下行走近似於二分查找的操作(由於樹的不完全平衡可能會略慢)
int getPos(int val) {
int u = root,l = 0;
while(u) {
if(d[u].v == val) return u;
l = u;
u = d[u].ch[val > d[u].v];
}
return l;
}
5.獲得前驅或後繼
根據樹的形態和大小關係得到下面代碼:
int getNearby(int u, bool f) {
splay(u, 0);
if(!d[u].ch[f]) throw "getNearby";
u = d[u].ch[f];
while(d[u].ch[!f]) u = d[u].ch[!f];
return u;
}
** 關於批量插入
會讓排序樹的高度不確定增加。。
先把插入的序列弄成一顆幾乎最佳BST然後按照這個BST的根節點插入
下面給出O(n)建立幾乎最佳BST的代碼
int * src;
int buildTree(int lef, int rig) {
if(lef > rig) return 0;
int mid = (lef + rig) >> 1;
int r = newNode();
d[r].v = src[mid];
d[r].sz = 1;
if(lef == rig)
return r;
d[r].ch[0] = buildTree(lef, mid - 1);
d[r].ch[1] = buildTree(mid + 1, rig);
d[d[r].ch[0]].fa = r;
d[d[r].ch[1]].fa = r;
d[r].sz += d[d[r].ch[0]].sz + d[d[r].ch[1]].sz;
return r;
}
以上大約就是排序樹最基本的通用操作了
splay 伸展樹
特點 : 我愛旋轉!
調整操作 : splay!
複雜度: 大約相當於Treap的2倍
splay通過splay進行調整
splay(x, g) 代表把x連續旋轉變換到到g的兒子位置(旋轉前x在g的子樹裏)
首先顯然只需要不停地while(d[x].fa != g) rotate(x)就可以了
那麼splay是如何防止樹退化成一條鏈的呢?
關鍵在於“雙旋”操作
當旋轉A時,A是A的父親B的x兒子, 同時B是B的父親C的x兒子(x == 左/右),那麼可以先旋轉B,在旋轉A,然後你就發現深度神奇地變小了0.0
比如:
要把F旋轉到A的左兒子, 發現F是D的左兒子,D是B的左兒子
於是先rotate(D),變成
然後rotate(F)
然後。。咦深度怎麼沒減小?? 莫非這東西不靠譜?
(那就對了!的確不靠譜)
其實大多數情況下對於一直向左或向右的單鏈,splay還是比較有效的。但是儘管如此,splay的平均深度比Treap高出3分之1(所以慢)
代碼大約長這樣:
void splay (int x, int g) {
while(d[x].fa != g) {
int y = d[x].fa;
int z = d[y].fa;
if(z != g) {
if((d[z].ch[1] == y) == (d[y].ch[1] == x))
rotate(y);
else rotate(x);
}
rotate(x);
}
if(g == 0) root = x;
else updata(x);
}
既然有更好的調整方法,那我們爲什麼要splay呢?
原因:splay很靈活
我們可以使用splay操作來改變,操作樹的形態來簡單地完成一些其他排序樹難以完成的操作。
1.splay刪除操作
void erase(int x) {
int p = getPos(x);
int lpos = getNearby(p, false);
int rpos = getNearby(p, true);
splay(lpos, 0);
splay(rpos, lpos);
d[rpos].ch[0] = 0;
splay(rpos, 0);
}
上面的單點刪除,獲取了x的位置,然後獲取x的前驅後繼,把前驅旋轉到根(0),然後把後繼旋轉到根的右兒子位置,那麼節點x必然在根的右兒子的左兒子並且x的子樹裏只有它自己,於是直接刪去d[rpos].ch[0] = 0;
擴廣一下,運用到區間刪除中
void eraseInterval(int lpos, int rpos) {
splay(lpos, 0);
splay(rpos, lpos);
d[rpos].ch[0] = 0;
splay(rpos, 0);
}
2.splay區間翻轉
像線段樹那樣打lazy標記
首先把開區間左座標splay到根
把右座標splay到根的左兒子
然後給根的左兒子的右兒子打上lazy標記表示翻轉
void reverseInterval(int lefp, int rigp) {
splay(lefp, 0);
splay(rigp, lefp);
if(d[rigp].ch[0]) d[d[rigp].ch[0]].rv ^= 1;
}
接下來每訪問一個節點前都要下傳懶標記
如果有懶標記要交換左右兒子
inline void pushdown(int x) {
if(!d[x].rv) return ;
swap(d[x].ch[0], d[x].ch[1]);
d[d[x].ch[0]].rv ^= 1;
d[d[x].ch[1]].rv ^= 1;
d[x].rv = 0;
}
…大概就那麼幾個特別操作
接下來是splay的模板以及“普通平衡樹”一題的運行效果
普通平衡樹(TYVJ1729)
你需要寫一種數據結構(可參考題目標題),來維護一些數,其中需要 提供以下操作:
1. 插入一個整數x
2. 刪除一個整數x(若有多個相同的數,只刪除一個)
3. 查詢整數x的排名(若有多個相同的數,輸出最小的排名),相同的數依次排名,不併列排名
4. 查詢排名爲x的數,排名的概念同3
5. 求x的前驅(前驅定義爲小於x,且最大的數),保證x有前驅
6. 求x的後繼(後繼定義爲大於x,且最小的數),保證x有後繼
代碼(兩年前寫的比較醜請見諒= =!)
#include<cstdio>
//#define TAG
#ifdef TAG
#include<time.h>
#include<windows.h>
#endif
#define FOR(x) for(int i=1;i<=x;i++)
#define INF 90100099
#define MAXN 601000
int ch[MAXN][2],fa[MAXN],k[MAXN],sz[MAXN],cnt[MAXN];
int tmp,root=0,top=1;
void getint(int&num)
{
char c;int flag=1;num=0;
while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;
while(c>='0'&&c<='9'){num=num*10+c-48;c=getchar();}
num*=flag;
}
void chi(int i){
printf("%d,%d ",k[ch[i][0]],k[ch[i][1]]);
}
void debug(int x){
if(x==-1)x=root;
puts("-========================-");
printf("index:%d\n",x);
printf("code:%d\n",k[x]);
printf("cnt:%d\n",cnt[x]);
printf("child:\n %d,%d\n",k[ch[x][0]],k[ch[x][1]]);
chi(ch[x][0]);chi(ch[x][1]);
puts("");
printf("father:%d\n",fa[x]);
printf("size:%d\n",sz[x]);
puts("-========================-");
}
void updata(int v){
sz[v]=sz[ch[v][0]]+sz[ch[v][1]]+cnt[v];
}
void rotato(int x){
int y=fa[x],z=fa[y];
bool f=(ch[y][1]==x);
ch[y][f]=ch[x][!f];
if(ch[y][f])fa[ch[y][f]]=y;
ch[x][!f]=y,fa[y]=x;
fa[x]=z;
if(z)ch[z][ch[z][1]==y]=x;
updata(y);
//updata(x);
//printf("rotato %d up->%d[sz%d]\n",x,ch[x][!f],sz[x]);
}
void splay(int a,int g){
int b,c;
for(;fa[a]!=g;rotato(a)){
b=fa[a];
c=fa[b];
if(c!=g){
if((ch[b][0]==a)==(ch[c][0]==b)){
rotato(b);
}
else rotato(a);
}
}
if(g==0)root=a;
if(!g)updata(a);
}
int nxtNod(int v,bool mod){
int x=root,y;
while(x!=0){
y=x;
if(k[x]==v)break;
x=ch[x][k[x]<v];
}
if((k[y]>v&&mod==1)||(k[y]<v&&mod==0))
return y;
splay(y,0);
int tmp=ch[y][mod];
while(ch[tmp][!mod])tmp=ch[tmp][!mod];
return tmp;
}
void insert(int v){
int x=root,y=0;
while(x!=0){
sz[x]++;
y=x;
if(k[x]==v){
cnt[x]++;
splay(x,0);
return;
}
if(k[x]<v)x=ch[x][1];
else if(k[x]>v)x=ch[x][0];
}
k[top]=v;
if(y)ch[y][v>k[y]]=top;
fa[top]=y;
cnt[top]=sz[top]=1;
top++;
splay(top-1,0);
return;
}
void deletf(int v){
int l=nxtNod(v,0);
int r=nxtNod(v,1);
splay(l,0);
splay(r,l);
cnt[ch[r][0]]--;
if(!cnt[ch[r][0]]){
ch[r][0]=0;
}
updata(ch[r][0]);
updata(r);
updata(l);
}
int rank(int v){//v的排名
int x=root,ret=0;
while(x!=0){
if(k[x]==v)return ret+sz[ch[x][0]];
if(k[x]>v) x=ch[x][0];
else ret+=sz[ch[x][0]]+cnt[x],x=ch[x][1];
}
return ret+x?0:1;
}
int rankval(int rk){//排名的數 2
int x=root,y;
if(sz[x]<rk)return 0;
while(x!=0){
//printf("rkget:%d(%d)\n",k[x],rk);
y=x;
if(sz[ch[x][0]]>=rk)x=ch[x][0];
else if(cnt[x]>=rk-sz[ch[x][0]])return x;
else rk-=sz[ch[x][0]]+cnt[x],x=ch[x][1];
}
return y;
}
int main(){
int n,ag1,ag2;
insert(INF);
//debug(1);
insert(-INF);
//freopen("in.txt","r",stdin);
scanf("%d",&n);
FOR(n){
getint(ag1);getint(ag2);
if(ag1==1)insert(ag2);
if(ag1==2)deletf(ag2);
if(ag1==3)printf("%d\n",rank(ag2));
if(ag1==4)printf("%d\n",k[rankval(ag2+1)]);
if(ag1==5)printf("%d\n",k[nxtNod(ag2,false)]);
if(ag1==6)printf("%d\n",k[nxtNod(ag2,true)]);
if(ag1==0)debug(ag2);
}
}
效率:
例題
1.[TYVJ1729]文藝平衡樹
你需要寫一種數據結構(可參考題目標題),來維護一個有序數列,其中需要提供以下操作:翻轉一個區間,例如原有序序列是5 4 3 2 1,翻轉區間是[2,4]的話,結果是5 2 3 4 1
splay區間翻轉裸題,思路見上文
注意時時調用pushdown!
代碼:
#include <cstdio>
#include <cstring>
#include <algorithm>
namespace splay {
using std :: swap;
const int MAXD = 110000;
struct Node {
int v, ch[2], fa, sz;
bool rv;
} d[MAXD];
int tot = 0, root = 0, maxium, minimum;
inline int newNode() {
memset(d + ++tot, 0, sizeof d[0]);
return tot;
}
inline void pushdown(int x) {
if(!d[x].rv) return ;
swap(d[x].ch[0], d[x].ch[1]);
d[d[x].ch[0]].rv ^= 1;
d[d[x].ch[1]].rv ^= 1;
d[x].rv = 0;
}
inline void updata(int x) {
d[x].sz = d[d[x].ch[0]].sz + d[d[x].ch[1]].sz + 1;
}
inline void rotate(int x) {
int y = d[x].fa;
bool f = (d[y].ch[1] == x);
d[x].fa = d[y].fa;
d[y].fa = x;
if(d[x].fa) d[d[x].fa].ch[d[d[x].fa].ch[1] == y] = x;
d[y].ch[f] = d[x].ch[!f];
if(d[x].ch[!f]) d[d[x].ch[!f]].fa = y;
d[x].ch[!f] = y;
updata(y);
updata(x);
}
void splay (int x, int g) {
while(d[x].fa != g) {
int y = d[x].fa;
int z = d[y].fa;
if(z != g) {
if((d[z].ch[1] == y) == (d[y].ch[1] == x))
rotate(y);
else rotate(x);
}
rotate(x);
}
if(g == 0) root = x;
else updata(x);
}
int * src;
int buildTree(int lef, int rig) {
if(lef > rig) return 0;
int u = newNode();
int mid = (lef+rig) >> 1;
d[u].v = src[mid];
if(lef == rig){
d[u].sz = 1;
return u;
}
d[u].ch[0] = buildTree(lef, mid - 1);
d[u].ch[1] = buildTree(mid + 1, rig);
if(d[u].ch[0]) d[d[u].ch[0]].fa = u;
if(d[u].ch[1]) d[d[u].ch[1]].fa = u;
updata(u);
return u;
}
int getByRank(int rk, int u = root) {
while(u) {
pushdown(u);
if(d[d[u].ch[0]].sz >= rk)
u = d[u].ch[0];
else if(d[d[u].ch[0]].sz + 1 == rk)
return u;
else rk -= d[d[u].ch[0]].sz + 1, u = d[u].ch[1];
}
return u;
}
int jump(int u, int rk) {
splay(u, 0);
if(d[u].ch[1] == 0) return 0;
return getByRank(rk, d[u].ch[1]);
}
void reverseInterval(int lefp, int rigp) {
//printf("rev: %d %d\n", lefp, rigp);
splay(lefp, 0);
splay(rigp, lefp);
if(d[rigp].ch[0]) d[d[rigp].ch[0]].rv ^= 1;
}
int * output;
void print(int u) {
if(!u) return ;
pushdown(u);
print(d[u].ch[0]);
*(output++) = d[u].v;
print(d[u].ch[1]);
}
void init() {
root = minimum = newNode();
d[minimum].ch[1] = maxium = newNode();
d[maxium].sz = 1;
d[minimum].sz = 2;
d[maxium].fa = minimum;
}
}
int buf[110000];
inline int getInt() {
int ret = 0;
char ch;
while((ch = getchar()) < '0' || ch > '9') ;
do {ret *= 10; ret += ch - '0';}
while((ch = getchar()) >= '0' && ch <= '9') ;
return ret;
}
int main() {
int n, m;
n = getInt(), m = getInt();
for(int i = 1; i<=n; i++)
buf[i] = i;
splay :: init();
splay :: src = buf;
splay :: d[splay :: maxium].ch[0]
= splay :: buildTree(1, n);
splay :: d[splay :: d[splay :: maxium].ch[0]].fa
= splay :: maxium;
splay :: d[splay :: maxium].sz += n;
splay :: d[splay :: minimum].sz += n;
int a, b;
for(int i = 1; i<=m; i++){
a = getInt();
b = getInt();
splay :: reverseInterval
(splay :: getByRank(a), splay :: getByRank(b + 2));
}
splay :: output = buf;
splay :: print(splay :: root);
for(int i = 1; i<=n; i++)
printf("%d ", buf[i]);
}
2.[NOI2003]文本編輯器
很久很久以前,DOS3.x的程序員們開始對EDLIN感到厭倦。於是,人們開始紛紛改用自己寫的文本編輯器……
多年之後,出於偶然的機會,小明找到了當時的一個編輯軟件。進行了一些簡單的測試後,小明驚奇地發現:那個軟件每秒能夠進行上萬次編輯操作(當然,你不能手工進行這樣的測試)!於是,小明廢寢忘食地想做一個同樣的東西出來。你能幫助他嗎?
爲了明確任務目標,小明對“文本編輯器”做了一個抽象的定義:
文本:由0個或多個字符構成的序列。這些字符的ASCII碼在閉區間[32, 126]內,也就是說,這些字符均爲可見字符或空格。
光標:在一段文本中用於指示位置的標記,可以位於文本的第一個字符之前,文本的最後一個字符之後或文本的某兩個相鄰字符之間。
文本編輯器:爲一個可以對一段文本和該文本中的一個光標進行如下六條操作的程序。如果這段文本爲空,我們就說這個文本編輯器是空的。
這道題充分體現了splay的靈活性
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
const int MAXL = 1024*1024*2;
namespace splay {
using std :: pair;
const int MAXD = MAXL;
void debug(int a, int b);
struct Node {
char v;
int fa, sz, ch[2];
} d[MAXD];
int tot = 0, maxium, minimum, root;
inline int newNode() {
memset(&d[++tot], 0, sizeof d[0]);
return tot;
}
inline void updata(int u) {
d[u].sz = d[d[u].ch[0]].sz + d[d[u].ch[1]].sz + 1;
}
inline int rotate(int x) {
int y = d[x].fa;
bool f = (d[y].ch[1] == x);
d[y].ch[f] = d[x].ch[!f];
if(d[y].ch[f])d[d[y].ch[f]].fa = y;
d[x].ch[!f] = y;
d[x].fa = d[y].fa;
d[y].fa = x;
if(d[x].fa) d[d[x].fa].ch[d[d[x].fa].ch[1] == y] = x;
updata(y);
updata(x);
//printf("rot:%d\n", x);
//getchar();
return x;
}
void splay(int x, int goal) {
while(d[x].fa != goal) {
int y = d[x].fa, z = d[y].fa;
if(z != goal) {
if((d[z].ch[0] == y) == (d[y].ch[0] == x))
rotate(y);
else rotate(x);
}
rotate(x);
}
if(goal == 0) root = x;
else updata(x);
}
int getNearby(int u, bool f) {
splay(u, 0);
if(!d[u].ch[f]) throw "getNearby";
u = d[u].ch[f];
while(d[u].ch[!f]) u = d[u].ch[!f];
return u;
}
int getByRank(int rk, int u = root) {
while(u) {
if(d[d[u].ch[0]].sz >= rk)
u = d[u].ch[0];
else if(d[d[u].ch[0]].sz + 1 >= rk)
return u;
else rk -= d[d[u].ch[0]].sz + 1, u = d[u].ch[1];
}
return u;
}
int jump(int u, int len) {
splay(u, 0);
return getByRank(len+1, d[u].ch[1]);
}
void insertDir (int id, int to) {
int t = getNearby(to, true);
//puts("bef:");
//debug(root, 0);
splay(t, 0);
//printf("to : %d, t : %d\n", to, t);
//debug(root, 0);
splay(to, t);
d[to].ch[1] = id;
d[id].fa = to;
while(to){
d[to].sz += d[id].sz;
to = d[to].fa;
}
}
char * src;
int buildTree(int lef, int rig) {
if(lef > rig) return 0;
int mid = (lef + rig) >> 1;
int r = newNode();
d[r].v = src[mid];
d[r].sz = 1;
if(lef == rig)
return r;
d[r].ch[0] = buildTree(lef, mid - 1);
d[r].ch[1] = buildTree(mid + 1, rig);
d[d[r].ch[0]].fa = r;
d[d[r].ch[1]].fa = r;
d[r].sz += d[d[r].ch[0]].sz + d[d[r].ch[1]].sz;
return r;
}
void eraseInterval(int lpos, int rpos) {
splay(lpos, 0);
splay(rpos, lpos);
d[rpos].ch[0] = 0;
splay(rpos, 0);
}
char * output;
void print(int u) {
if(!u) return;
print(d[u].ch[0]);
*output = d[u].v;
++ output;
print(d[u].ch[1]);
}
char * getInterval(char * ans, int lpos, int rpos) {
splay(lpos, 0);
splay(rpos, lpos);
output = ans;
print(d[rpos].ch[0]);
*output = '\0';
return ans;
}
void clear() {
tot = 0;
minimum = root = newNode();
maxium = d[root].ch[1] = newNode();
d[maxium].sz = 1;
d[minimum].sz = 2;
d[maxium].fa = minimum;
}
void debug(int u, int dep = 0) {
if(!u) return;
debug(d[u].ch[0], dep + 1);
printf("[%d](%d) u:%d, sz:%d\n", dep, d[u].v, u, d[u].sz);
debug(d[u].ch[1], dep + 1);
}
}
char * readChars(char * str, int c) {
char * r = str;
while(c --){
*str = getchar();
if(*str == '\r' || *str == '\n') c ++;
else ++str;
}
return r;
}
inline int getInt() {
int ret = 0;
char ch;
while((ch = getchar()) < '0' || ch > '9') ;
do {ret *= 10; ret += ch - '0';}
while((ch = getchar()) >= '0' && ch <= '9') ;
ungetc(ch, stdin);
return ret;
}
char buf[MAXL];
int main () {
splay :: clear();
int cur = splay :: minimum;
int n;
scanf("%d", &n);
for(int i = 1; i<=n; i++) {
scanf("%s", buf);
if(buf[0] == 'I'){
int len = getInt();
splay :: src = readChars(buf, len);
splay :: insertDir(
splay :: buildTree(0, len - 1), cur);
} else if(buf[0] == 'N')
cur = splay :: getNearby(cur, true);
else if(buf[0] == 'P')
cur = splay :: getNearby(cur, false);
else if(buf[0] == 'D')
splay :: eraseInterval(cur,
splay :: jump(cur, getInt()));
else if(buf[0] == 'M')
cur = splay :: getByRank(getInt() + 1);
else if(buf[0] == 'G')
printf("%s\n", splay :: getInterval(buf, cur,
splay :: jump(cur, getInt())));
else if(buf[0] == '*') splay :: debug(splay :: root);
}
}
Treap
(身體有點不舒服。。下次在更Treap, SBT)