二叉查詢樹和笛卡爾樹

_kilo-meteor發表於2024-10-18

目錄
  • 二叉查詢樹
    • 定義
    • 作用
    • 操作
      • 查詢
      • 插入
      • 刪除
    • 缺點
  • 笛卡爾樹
    • 定義
    • 操作
      • 構造

二叉查詢樹

定義

二叉查詢樹(Binary Search Tree,BST),又名二叉搜尋樹或二叉排序樹。

​ 它是一類特殊規定的二叉樹,它應當滿足以下條件:

  1. 每個節點有唯一確定的權值
  2. 非葉子節點的權值比其左子樹中所有節點權值大
  3. 非葉子節點的權值比其右子樹中所有節點權值小

​ 由於上述特性,易知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是一棵滿二叉樹:Annotation 2023-03-25 105753

​ 而序列1234567就會退化成一條鏈:Annotation 2023-03-25 110026

​ 此外,在刪除的過程中,還會改變樹的形態,也可能會使它退化。

​ BST存在的問題即“如何保持平衡的形態”。後續的替罪羊樹、Treap樹、Splay樹、紅黑樹等等都是基於“維護平衡”這一問題的BST樹最佳化演算法。

笛卡爾樹

定義

笛卡爾樹是一類特殊的二叉查詢樹,其和一般BST的區別在於每個節點包括兩個權值資訊。

一棵笛卡爾樹應當滿足如下條件:

  1. 每個節點包括兩個唯一確定的權值\((x_u,y_u)\)
  2. 只考慮權值\(x_u\)的情況下,樹的形態應當符合一棵二叉查詢樹的性質。
  3. 只考慮權值\(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;
} // 此題的噁心之處在於必須快讀

相關文章