替罪羊樹(Scapegoat Tree)
html
入門模板題 洛谷oj P3369node
題目描述ios
您須要寫一種數據結構(可參考題目標題),來維護一些數,其中須要提供如下操做:數組
- 插入xx數
- 刪除xx數(如有多個相同的數,因只刪除一個)
- 查詢xx數的排名(排名定義爲比當前數小的數的個數+1+1。如有多個相同的數,因輸出最小的排名)
- 查詢排名爲xx的數
- 求xx的前驅(前驅定義爲小於xx,且最大的數)
- 求xx的後繼(後繼定義爲大於xx,且最小的數)
輸入格式網絡
第一行爲n,表示操做的個數,下面n行每行有兩個數opt和x,opt表示操做的序號( 1≤opt≤6 )數據結構
輸出格式學習
對於操做3,4,5,6每行輸出一個數,表示對應答案優化
輸入樣例ui
10spa
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598輸出樣例
106465
84185
492737數據範圍
1.n的數據範圍: n≤100000
2.每一個數的數據範圍: [-10^7, 10^7]
網上的資料比較瑣碎難懂,以前看了不少資料一直不能理解平衡樹(我太弱了)……前幾天忽然莫名其妙明白了,想寫一篇筆記記錄一下(亂寫一通)。
0x00 二叉查找樹
要初步弄懂平衡樹,首先要知道這是一棵二叉查找樹。
二叉查找樹(Binary Search Tree),固然也能夠叫它二叉搜索樹,或者二叉排序樹(反正都一個意思都是二叉樹),它的定義以下:
或者是一棵空樹,或者是具備下列性質的二叉樹:
差很少就像下面這幅圖同樣:
0x01 平衡樹的用途
在學平衡樹這個數據結構前,相信咱們必定會先有個問題:平衡樹能拿來幹什麼?
網上的不少資料對這一點寫得不太明白(也多是我太弱了),我先試着亂總結一下:
如今須要一種數據結構,它須要作到如下幾點:
1. 高效地查詢一個序列中某個數的前面和後面的數(a[i-1]和a[i+1])。
2. 高效地知道第i個數是什麼(即a[i])。
3. 高效地插入和刪除。
顯然,咱們能夠用普通數組優秀地完成第1點和第2點,但第3點不可以了。固然,咱們也能夠用鏈表,複雜度O(1)優秀地完成第1點和第3點,可是對於第2點,鏈表的複雜度就達到了不太理想的O(n)。
那咱們能不能想辦法優化一下鏈表呢?
咱們能夠回頭看一下二叉搜索樹的定義,而後咱們會發現,假設存在一個從1到8的鏈表(就像下圖,win自帶畫圖畫的,不太好看)
其實它也知足左邊小於右邊的定義,也能夠勉強算是一棵以序號爲每一個節點的權值的二叉查找樹。
可是這樣的一棵二叉樹是否是很難看很畸形?
咱們怎麼想辦法把它變成一棵比較好看的樹呢?
能夠拿筆畫畫看:
咱們嘗試把中間的節點(4或者5,我選擇4)拎出來,而後就變成了這樣:
是否是好看了一點?有點樹的形狀了,那咱們能夠嘗試繼續把兩側鏈上的中間節點繼續拎出來,不斷重複,最終會變成一棵比較好看的樹。
這就是平衡二叉樹,嚴格遵照了二叉查找樹的定義——左兒子小於右兒子。
也許你會有疑問:長成這樣的一棵樹,怎麼作到剛剛鏈表都不能徹底作到的3點要求呢?
讓咱們再看看這棵樹:
0x02 查詢前驅和後驅
對於一個咱們已知的節點i,咱們先定義與它深度相同的都是它的兄弟節點。
那麼很顯然,i的左兄弟及其子樹上的全部節點都比i的左兒子及其子樹上的全部節點來得小,且i的左兒子及其子樹上的全部點都比i的父親更大,因此顯然,i的前驅要在i的左兒子及其子樹上找。
同理,仔細觀察圖,會發現咱們所要找的前驅,存在於i的左兒子子樹的最右側,就是i的左兒子的右兒子的右兒子的右兒子的右兒子……(直到最後一個右兒子)
這樣,咱們就能夠O(log n)地求出i的前驅了。i的後驅同理。
0x03 查詢第i個數
前面說過,咱們用數的序列編號做爲節點的排序權值,因此咱們只要像線段樹那樣從根部開始查一遍就能夠了。詳細解釋:
從根結點開始,若是第i個數比當前節點j的序號小,就往左兒子搜,反之右兒子。直到找到第i個數,時間複雜度仍是O(log n)。
0x04 插入和刪除
插在原序列的末尾,因此新節點的編號是n+1。而後咱們把這個節點變成根結點的右兒子的右兒子的右兒子……(變成最後一個沒有右兒子的節點的右兒子)。
刪除。其實就是把i節點打個被刪除掉的標記(甚至不打也能夠)。而後讓i的左右兒子之一成爲i的父節點的新兒子(就是讓某一個兒子取代i節點)。複雜度同O(log n)
基本操做差很少就是這樣。
那麼其實還有一個問題:若是操做太多,致使一棵原本平衡的樹變成了一條鏈表,複雜度爆炸,怎麼辦呢?
0x05 從新建樹
若是你選擇的是替罪羊樹,那就是優雅的暴力了。替罪羊樹在每一個節點上記錄子樹的節點數size,同時還有一個平衡因子alpha(一般在0.5左右,我選擇0.7),當每次更新後,遞歸回去檢查i節點的左右兒子分別乘以平衡因子,是否大於另外一個兒子,若是大了,表明這棵樹有退化的傾向,趕忙拍平重建(就是把樹壓成鏈表,從新建樹)。
大概就是這樣,手機打的好難受,直接上代碼好了。
(其實我一開始作平衡樹時以爲能夠用線段樹模擬的emmm,就不說了)
代碼能夠配合洛谷的模板題食用
#include <cstdio> #include <iostream> using namespace std; const int INF=1000000000; const int MAX_N=2000005; const double alpha=0.75; int n; inline int read(){ register int ch=getchar(),x=0,f=1; while (ch<'0'||ch>'9'){ if (ch=='-') f=-1; ch=getchar(); } while (ch>='0'&&ch<='9'){ x=x*10+ch-'0'; ch=getchar(); } return x*f; } struct Tree{ int fa; int size; int value; int son[2]; }tree[MAX_N]; int cnt=2; int root=1; int node[MAX_N]; int sum; bool balance(int x){ //判斷是否平衡 return (double)tree[x].size*alpha>=(double)tree[tree[x].son[0]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[1]].size; } int build(int l,int r){ //從新遞歸建樹 if (l>r) return 0; int mid=(l+r)>>1; tree[tree[node[mid]].son[0]=build(l,mid-1)].fa=node[mid],tree[tree[node[mid]].son[1]=build(mid+1,r)].fa=node[mid]; tree[node[mid]].size=tree[tree[node[mid]].son[0]].size+tree[tree[node[mid]].son[1]].size+1; return node[mid]; } void recycle(int x){ //把樹壓成數列 if (tree[x].son[0]) recycle(tree[x].son[0]); node[++sum]=x; if (tree[x].son[1]) recycle(tree[x].son[1]); } void rebuild(int x){ sum=0; recycle(x); int fa=tree[x].fa,son=(tree[tree[x].fa].son[1]==x),now=build(1,sum); tree[tree[fa].son[son]=now].fa=fa; if (x==root) root=now; } void insert(int x){ int i=root,now=++cnt; //新節點序號 tree[now].size=1,tree[now].value=x; while (true){ tree[i].size++; bool son=(x>=tree[i].value); if (tree[i].son[son]) i=tree[i].son[son]; else{ tree[tree[i].son[son]=now].fa=i; break; } } int flag=0; for (int j=now;j;j=tree[j].fa) //logn找不平衡的節點 if (!balance(j)) flag=j; if (flag) rebuild(flag); //重建樹 } int get_num(int x){ int i=root; while (true){ if(tree[i].value==x) return i; else i=tree[i].son[tree[i].value<x]; } } void erase(int x){ //刪除 if (tree[x].son[0]&&tree[x].son[1]){ int now=tree[x].son[0]; while (tree[now].son[1]) now=tree[now].son[1]; tree[x].value=tree[now].value; x=now; } int son=(tree[x].son[0])?tree[x].son[0]:tree[x].son[1]; int k=(tree[tree[x].fa].son[1]==x); tree[tree[tree[x].fa].son[k]=son].fa=tree[x].fa; for (int i=tree[x].fa;i;i=tree[i].fa) tree[i].size--; if (x==root) root=son; } int get_rank(int x){ int i=root,ans=0; while (i) if(tree[i].value<x) ans+=tree[tree[i].son[0]].size+1,i=tree[i].son[1]; else i=tree[i].son[0]; return ans; } int get_kth(int x){ int i=root; while (true) if (tree[tree[i].son[0]].size==x-1) return i; else if (tree[tree[i].son[0]].size>=x) i=tree[i].son[0]; else x-=tree[tree[i].son[0]].size+1,i=tree[i].son[1]; return i; } int get_front(int x){ int i=root,ans=-INF; while(i) if(tree[i].value<x) ans=max(ans,tree[i].value),i=tree[i].son[1]; else i=tree[i].son[0]; return ans; } int get_behind(int x){ int i=root,ans=INF; while(i) if(tree[i].value>x) ans=min(ans,tree[i].value),i=tree[i].son[0]; else i=tree[i].son[1]; return ans; } int main(){ // freopen("test1.in","r",stdin); tree[1].value=-INF,tree[1].size=2,tree[1].son[1]=2; tree[2].value=INF,tree[2].size=1,tree[2].fa=1; n=read(); for(int i=1,op,x;i<=n;i++){ op=read(),x=read(); if(op==1) insert(x); if(op==2) erase(get_num(x)); if(op==3) printf("%d\n",get_rank(x)); if(op==4) printf("%d\n",tree[get_kth(x+1)].value); if(op==5) printf("%d\n",get_front(x)); if(op==6) printf("%d\n",get_behind(x)); } }
其餘例題
大致上按難度排序?我太弱了也搞不懂。
一.[HNOI2002]營業額統計 (2019.4.10更新)
聽說有各類神犇用許多神奇的解法A掉了……可是我這種蒟蒻就先用平衡樹練手了。
min{|該天之前某一天的營業額-該天營業額|},即不大於a[i]的最大值和不小於a[i]的最小值,就是尋找a[i]的前驅和後驅,分別減去a[i]取絕對值,再所有加起來,複雜度應該是O(nlogn)。
代碼:
#include <cstdio> #include <iostream> using namespace std; const int INF=1000000000; const int MAX_N=2000005; const double alpha=0.75; int n; inline int read(){ register int ch=getchar(),x=0,f=1; while (ch<'0'||ch>'9'){ if (ch=='-') f=-1; ch=getchar(); } while (ch>='0'&&ch<='9'){ x=x*10+ch-'0'; ch=getchar(); } return x*f; } struct Tree{ int fa; int size; int value; int son[2]; }tree[MAX_N]; int cnt=2; int root=1; int node[MAX_N]; int sum; bool balance(register int x){ //判斷是否平衡 return (double)tree[x].size*alpha>=(double)tree[tree[x].son[0]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[1]].size; } inline int build(register int l,register int r){ //從新遞歸建樹 if (l>r) return 0; int mid=(l+r)>>1; tree[tree[node[mid]].son[0]=build(l,mid-1)].fa=node[mid],tree[tree[node[mid]].son[1]=build(mid+1,r)].fa=node[mid]; tree[node[mid]].size=tree[tree[node[mid]].son[0]].size+tree[tree[node[mid]].son[1]].size+1; return node[mid]; } void recycle(register int x){ //把樹壓成數列 if (tree[x].son[0]) recycle(tree[x].son[0]); node[++sum]=x; if (tree[x].son[1]) recycle(tree[x].son[1]); } void rebuild(register int x){ sum=0; recycle(x); int fa=tree[x].fa,son=(tree[tree[x].fa].son[1]==x),now=build(1,sum); tree[tree[fa].son[son]=now].fa=fa; if (x==root) root=now; } void insert(register int x){ int i=root,now=++cnt; //新節點序號 tree[now].size=1,tree[now].value=x; while (true){ tree[i].size++; bool son=(x>=tree[i].value); if (tree[i].son[son]) i=tree[i].son[son]; else{ tree[tree[i].son[son]=now].fa=i; break; } } int flag=0; for (int j=now;j;j=tree[j].fa) //logn找不平衡的節點 if (!balance(j)) flag=j; if (flag) rebuild(flag); //重建樹 } inline int get_front(register int x){ register int i=root,ans=-INF; while(i) if(tree[i].value<x) ans=max(ans,tree[i].value),i=tree[i].son[1]; else i=tree[i].son[0]; return ans; } inline int get_behind(register int x){ register int i=root,ans=INF; while(i) if(tree[i].value>x) ans=min(ans,tree[i].value),i=tree[i].son[0]; else i=tree[i].son[1]; return ans; } bool flag[1000000+5]; int result; int main(){ // freopen("test1.in","r",stdin); tree[1].value=-INF,tree[1].size=2,tree[1].son[1]=2; tree[2].value=INF,tree[2].size=1,tree[2].fa=1; n=read(); int kkk=read(); result+=kkk; insert(kkk); flag[kkk]=true; for (int i=2,a,min,max;i<=n;i++){ a=read(); if (flag[a]) continue; flag[a]=true; insert(a); min=get_front(a); max=get_behind(a); if (min==-INF){ result+=(max-a); continue; } else if (max==INF){ result+=(a-min); continue; } result+=(a-min>max-a?max-a:a-min); } printf("%d",result); return 0; }
[學習自百度百科和其餘網絡資料]