- 二叉查詢樹
- 定義
- 作用
- 操作
- 查詢
- 插入
- 刪除
- 缺點
- 笛卡爾樹
- 定義
- 操作
- 構造
二叉查詢樹
定義
二叉查詢樹(Binary Search Tree,BST),又名二叉搜尋樹或二叉排序樹。
它是一類特殊規定的二叉樹,它應當滿足以下條件:
- 每個節點有唯一確定的權值
- 非葉子節點的權值比其左子樹中所有節點權值大
- 非葉子節點的權值比其右子樹中所有節點權值小
由於上述特性,易知BST的中序遍歷是一個有序排列。
特別地,空樹也是一棵BST(覺得有意思就記下來了?)
作用
顧名思義,它用於快速地查詢資料,同時它也支援快速地插入與刪除資料。
因為在BST上查詢一個數,所需的查詢次數不超過樹的深度。查詢過程類似於二分,運氣好的話,它的時間複雜度能和二分相同。
實際上,由於原序列本身無序,所以每次查詢的時間複雜度會在\(O(\log n)\)到\(O(n)\)之間浮動。(具體原因後面再寫)
操作
查詢
透過遞迴實現。
二分思想中,每一次會在區間[l,r]中取mid,將區間均分為兩半。並對mid進行檢查,以此決定接下來是查詢左區間還是右區間。
(說起來,初中數學有教過二分吧?)
對應的,在BST中,當前查詢到的節點相當於區間的mid,將區間分為左子樹和右子樹兩部分(遺憾的是,並非均分)。對當前節點進行“判斷”,就可以決定接下來的查詢範圍是棗子樹還是柚子樹。
(好耶,是棗子柚子二選一!)
(試圖把左子樹稱為棗子樹,把右子樹稱為柚子樹)
插入
在查詢操作的基礎上加一點點東西。
如果按照給定順序和值(即給定序列)插入,最終建成的BST一定是唯一的。
對於需要插入的資料,我們可以透過查詢得到它“應該在的位置”。如果這個位置是空位,就在這個位置插入新節點。
(如果不是空位,說明之前已經有同樣的資料,可以按照需要進行記錄。例如在每個節點用int cnt記錄資料出現次數)
刪除
有刪除操作時,BST可能不唯一。
好在有兩種輕鬆愉悅的情況:
1.如果要刪除的節點孤苦伶仃,無兒無女,無依無靠,那直接把它刪了就行,反正也沒節點給它收屍不是嗎。
2.如果要刪除的節點只有棗子樹或只有柚子樹(總之就是隻有一個兒子),那麼直接刪除它,並將子樹銜接上來,代替它的位置。
其餘情況比較麻煩(兒孫們要爭奪祖父的地位(?))
這裡列舉兩種常見的解決方法,同樣是遞迴操作:
其一是:用左子樹中的最大節點,頂替刪除節點。但是對於左子樹來說,這相當於刪除了“最大節點”,所以繼續遞迴,直到前面兩種情況之一。
其二是:用右子樹中的最小節點,頂替刪除節點。同理,繼續遞迴。
由於刪除時的操作,BST可能變成奇奇怪怪的形狀。
缺點
形態不穩定。
前面有說過,它每次操作的時間複雜度最好是\(O(\log n)\),最差會到\(O(n)\)。這是因為插入序列的順序不一定,按照它建出的BST可能恰好平衡,也可能退化成一條鏈。
比如序列4213657
是一棵滿二叉樹:
而序列1234567
就會退化成一條鏈:
此外,在刪除的過程中,還會改變樹的形態,也可能會使它退化。
BST存在的問題即“如何保持平衡的形態”。後續的替罪羊樹、Treap樹、Splay樹、紅黑樹等等都是基於“維護平衡”這一問題的BST樹最佳化演算法。
笛卡爾樹
定義
笛卡爾樹是一類特殊的二叉查詢樹,其和一般BST的區別在於每個節點包括兩個權值資訊。
一棵笛卡爾樹應當滿足如下條件:
- 每個節點包括兩個唯一確定的權值\((x_u,y_u)\)
- 只考慮權值\(x_u\)的情況下,樹的形態應當符合一棵二叉查詢樹的性質。
- 只考慮權值\(y_u\)的情況下,樹的形態應當符合大根堆或小根堆的性質。
根據OI Wiki的說明,如果一棵笛卡爾樹的\((x_u,y_u)\)唯一確定,那麼這棵笛卡爾樹的形態唯一。
關於唯一確定:即\((x_u,y_u)\)均已知,且\(x_u\)互不相同,\(y_u\)互不相同。
操作
構造
首先將節點按照\(x_u\)從小到大的順序排序,現在假設我們要構造的笛卡爾樹符合小根堆的性質。
由於二叉查詢樹的性質,且\(x_u\)遞增,顯然新節點的位置應該儘可能靠右。可以透過維護從根開始的一條極長鏈,滿足每個節點都是其父節點的右兒子。
這條鏈滿足鏈上節點的權值\(x,y\)均單調遞增,可以看作一個單調棧,或者說可以用單調棧來維護。加入新節點\(u\)的時候,在鏈上找到深度最大的節點\(v\)滿足\(y_v<y_u\),將其作為\(u\)的父節點。顯然\(x_u>x_v\),故令\(u\)為\(v\)的右兒子。
如果\(v\)有右兒子,則將\(v\)的右子樹拆下來,接在\(u\)的左子樹下。由於\(v\)是深度最大的\(y<y_u\),所以\(y_v<y_u<y_{rs(v)}\),且\(x_u>x_{rs(v)}\),所以這樣操作不會破壞笛卡爾樹的性質。
以洛谷P5854【模板】笛卡爾樹為例,程式碼如下:
#include <bits/stdc++.h>
using namespace std;
#define lld long long
const int N = 1e7+5;
int n, q[N], tl = 0;
struct Tree{
int val, ls, rs;
}t[N];
void read(int &x){
char input = getchar(); x = 0;
while(input<'0'||input>'9')
input = getchar();
while(input>='0'&&input<='9'){
x = x*10+(input-'0');
input = getchar();
}
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; i++){
int T = tl; read(t[i].val);
while(tl && t[q[tl]].val > t[i].val) tl--;
if(tl) t[q[tl]].rs = i;
if(tl < T) t[i].ls = q[tl+1];
q[++tl] = i;
}
lld ans1 = 0, ans2 = 0;
for(int i = 1; i <= n; i++){
ans1 ^= (lld)i*(t[i].ls+1);
ans2 ^= (lld)i*(t[i].rs+1);
} printf("%lld %lld\n", ans1, ans2);
return 0;
} // 此題的噁心之處在於必須快讀