笛卡爾樹
大部分內容來自 OI-WIKI
定義:
笛卡爾樹是一種二叉樹,每一個結點由一個鍵值二元組 \((k,w)\) 構成。
要求 \(k\) 滿足二叉搜索樹的性質,而 \(w\) 滿足堆的性質。
如果笛卡爾樹的 \(k,w\) 鍵值確定,\(k,w\) 互不相同,那么這個笛卡爾樹的結構是唯一的。
上面這棵笛卡爾樹相當於把數組元素值當作鍵值 \(w\) ,把數組下標當作鍵值 \(k\) .
顯然可以發現:這棵樹的鍵值 \(k\) 滿足二叉搜索樹的性質,而鍵值 \(w\) 滿足小根堆的性質。
其實這是一種特殊的笛卡爾樹,鍵值 \(k\) 正好對應數組下標。更一般的情況則是任意二元組構建的笛卡爾樹。
構建:
棧構建:
將元素按鍵值 \(k\) 排序(也就是下標),先后插入當前的笛卡爾樹中,那么我們每次插入的元素必然在這個樹的右鏈(從根節點一直往右子樹走形成的鏈) 的末端。
執行這樣一個過程:
- 從下往上比較右鏈節點與當前加入節點 \(x\) 的鍵值 \(w\):
-
如果找到右鏈上的節點 \(u\) 滿足 \(w_u<w_x\) ,那么把 \(x\) 接到 \(u\) 的右兒子上,而以 \(u\) 為根的右子樹變成了 \(x\) 的左子樹,然后停止加入過程。
-
如果沒有找到右鏈上的節點,那么 \(x\) 就是新根,把原來的樹當成 \(x\) 的左兒子。
每次添加值都需要進行一次這樣的比較。
過程圖:
顯然,每個數最多進出右鏈一次,這個過程可以用棧進行維護,棧中維護當前笛卡爾樹右鏈上的節點,不在右鏈就彈出,時間復雜度為 \(O(n)\)
代碼:
for(int i=1;i<=n;i++){
while(top&&a[stk[top]]>a[i]) son[i][0]=stk[top--];//當前節點成為了i的左兒子
if(stk[top]) son[stk[top]][1]=i;//還存在,說明沒有遍歷到頭,因此該點對應的右兒子設置成i
stk[++top]=i;//新兒子進節點
}
例題:
題意:
給定一個 \([1-n]\) 的排列 \(p\), 構建一顆二叉樹滿足:
- 每個節點編號滿足二叉搜索樹性質
- 節點 \(i\) 的權值為 \(p_i\) ,每個節點權值滿足小根堆性質。
分別求每個節點左 \(/\) 右兒子乘節點編號的異或值之和
分析:
就是構建一顆笛卡爾樹,然后計算就行了:
代碼:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e7+5;
int n;
int a[N],son[N][2];
int sta[N],top;
int rans,lans;
int read(){//1e7的讀入....記得快讀
int res=0; char ch=getchar();
while(ch>'9'||ch<'0') ch=getchar();
while(ch<='9'&&ch>='0'){
res=(res<<1)+(res<<3)+ch-'0'; ch=getchar();
}
return res;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
a[i]=read();
while(a[sta[top]]>a[i]&&top) son[i][0]=sta[top--];
if(sta[top]) son[sta[top]][1]=i;
sta[++top]=i;
}
for(int i=1;i<=n;i++){
lans=lans^(i*(son[i][0]+1));
rans=rans^(i*(son[i][1]+1));
}
cout<<lans<<" "<<rans<<endl;
system("pause");
return 0;
}
這里要注意的是:
-
棧中存儲的每個節點的標號,並不是該節點對應的值
-
一定要弄清楚左右兒子的賦值情況。
性質(小根笛卡爾樹):
-
以點 \(u\) 為根的子樹是一段連續極長區間,\(w_x\) 是區間的最小值,區間在保證最小值不變的情況下不能再向兩邊延長。
-
區間 \([a,b]\) 的最小值為 \(w_{lca_{a,b}}\)
-
對於有序數列 \(x\) 及隨機排列 \(y\) ,笛卡爾樹的期望高度為:
\(E(dep_i)=H(i)+H(n-i+1)\) ,其中調和級數 \(H(n)=\sum_{i=1}^x \frac{1}{i}\)
關於這個可以看看 [AGC028B] Removing Blocks 這道題,我在博客里也寫了題解。
總結:
笛卡爾樹主要用於解決最大/最小值問題,以及通過記錄時間關於插入刪除的問題。