树论杂记
笛卡尔树
笛卡尔树和笛卡尔的关系就像雷峰塔和雷锋一样,一点关系也没有(
笛卡尔树的概念
笛卡尔树本质是一种固定结构的二叉搜索树,同时具有堆的性质。
笛卡尔树是一种二叉树,每一个节点由一个键值二元组 $ (k,w) $ 构成.要求 \(k\) 满足二叉搜索树(BST)的性质,而 \(w\) 满足堆的性质.如果笛卡尔树的 \(k,w\) 键值确定,且 \(k\) 互不相同,\(w\) 也互不相同,那么这棵笛卡尔树的结构是唯一的。
以上是 OI Wiki 中对于笛卡尔树的定义,其实笛卡尔树就是二叉搜索树的固定形式,treap等数据结构也可以视为笛卡尔树。
平时使用时,通常将下标 \(i\) ,作为键值 \(k\) ,将元素值作为 \(w\)。
依据笛卡尔树升序降序要求的不同,也可以将笛卡尔树分为大根笛卡尔树和小根笛卡尔树。
下图即为一颗满足小根堆性质的笛卡尔树。

笛卡尔树的建树
通常使用单调栈构建笛卡尔树,时间复杂度为\(O(n)\)。
以下构建过程会建出一颗满足小根堆性质的笛卡尔树,想要构建满足大根堆性质的笛卡尔树同理。
考虑从左至右构建笛卡尔树的过程中,每次加入一个元素,它只会加入到右链中(前面的所有元素均处于它的左边),那么所有节点的左链一定都是不变的,所以只需要动态维护右链。
用单调栈维护右链,则每次新加入一个节点,就不断弹栈,知道第一个小于当前元素键值\(w\),就将新加入的元素设为该节点的右儿子,将最后一个弹出栈的元素设为当前元素的左儿子。
对于每个至多只会入栈,出栈各一次,时间复杂度\(O(n)\)。
该方法也可以拓展至构建平衡树,即\(O(n)\)建出一颗平衡树。
此方法可用于当插入操作很少,\(O(n \log n)\)建树会成为瓶颈时,或者一次插入大量连续的元素时,可以先建出平衡树,再合并。
笛卡尔树相关例题
P5854 【模板】笛卡尔树
link.
单调栈建笛卡尔树板子题。
Code
#include <bits/stdc++.h>
#define int long long
#define ll long long
#define ull unsigned long long
#define inf 2e9
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N = 1e7 + 5,M = 1e6 + 5;
const int dx[4] = {0,0,1,-1},dy[4] = {1,-1,0,0};
int n,rt,p[N],son[N][2];
inline int build(){
stack<int> stk;
p[0] = -inf , stk.push(0);
for(int i = 1;i <= n;i++){
int lst = 0;
while(!stk.empty() && p[stk.top()] > p[i])
lst = stk.top() , stk.pop();
son[stk.top()][1] = i , son[i][0] = lst , stk.push(i);
}
return son[0][1];
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int tc = 1;
while(tc--){
cin >> n;
for(int i = 1;i <= n;i++) cin >> p[i];
rt = build();
int ans1 = 0,ans2 = 0;
for(int i = 1;i <= n;i++){
ans1 = ans1 ^ (i * (son[i][0] + 1));
ans2 = ans2 ^ (i * (son[i][1] + 1));
}
cout << ans1 << " " << ans2 << "\n";
}
return 0;
}
P1377 [TJOI2011] 树的序
link.
理解一下题面,问题本质是以 \(k_i\) 为键值 \(k\) (满足BST的性质),以 \(i\) 为键值 \(w\) (满足小根堆的性质),那么可以根据给出的生成序列建出笛卡尔树。
树的形态不能改变,那么父亲节点一定要先于子节点,同时要让新字典序最小,那么先分配给左子树是优于先分配右子树的,也就是在笛卡尔树上前序遍历(Root - L - R)。
时间复杂度:\(O(n)\)。
Code
#include <bits/stdc++.h>
#define int long long
#define ll long long
#define ull unsigned long long
#define inf 2e9
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N = 1e5 + 5,M = 1e6 + 5;
const int dx[4] = {0,0,1,-1},dy[4] = {1,-1,0,0};
int n,rt,p[N],son[N][2];
inline int build(){
stack<int> stk;
p[0] = -inf , stk.push(0);
for(int i = 1;i <= n;i++){
int lst = 0;
while(!stk.empty() && p[stk.top()] > p[i])
lst = stk.top() , stk.pop();
son[stk.top()][1] = i , son[i][0] = lst , stk.push(i);
}
return son[0][1];
}
void dfs(int x){
cout << x << " ";
if(son[x][0]) dfs(son[x][0]);
if(son[x][1]) dfs(son[x][1]);
return ;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int tc = 1;
while(tc--){
cin >> n;
for(int i = 1,x;i <= n;i++) cin >> x , p[x] = i;
rt = build() , dfs(rt);
}
return 0;
}
P2171 Hz 吐泡泡
link.
这道题目与P1377 [TJOI2011] 树的序很相似。
考虑 \(a_i\) 满足键值 \(k\) (BST) , \(i\) 满足键值 \(w\)(堆)。
所以将原序列按 \(a_i\) 排序后,用单调栈构建笛卡尔树。
最后后序遍历输出即可,时间复杂度:\(O(n)\)。
Code
#include <bits/stdc++.h>
#define int long long
#define ll long long
#define ull unsigned long long
#define inf 2e9
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N = 3e5 + 5,M = 1e6 + 5;
const int dx[4] = {0,0,1,-1},dy[4] = {1,-1,0,0};
int n,rt,son[N][2];
int ans[N],dep[N],top,deep;
struct node{
int x,id;
friend bool operator < (node x,node y){ return x.x < y.x; }
}p[N];
inline int build(){
stack<int> stk;
p[0].id = -inf , p[0].x = 0 , stk.push(0);
for(int i = 1;i <= n;i++){
int lst = 0;
while(!stk.empty() && p[stk.top()].id > p[i].id)
lst = stk.top() , stk.pop();
son[stk.top()][1] = i , son[i][0] = lst , stk.push(i);
}
return son[0][1];
}
void dfs(int x){
if(son[x][0]) dep[son[x][0]] = dep[x] + 1 , dfs(son[x][0]);
if(son[x][1]) dep[son[x][1]] = dep[x] + 1 , dfs(son[x][1]);
ans[++top] = x , deep = max(deep , dep[x]);
return ;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int tc = 1;
while(tc--){
cin >> n;
for(int i = 1,x;i <= n;i++) cin >> p[i].x , p[i].id = i;
sort(p + 1,p + n + 1);
rt = build() , dep[rt] = 1 , dfs(rt);
cout << "deep=" << deep << "\n";
for(int i = 1;i <= n;i++) cout << p[ans[i]].x << "\n";
}
return 0;
}
P14989 传送
link.
由题目中边的设定以及与最大值有关可以联想到笛卡尔树。
考虑对于点 \(i\) ,记左右与 \(i\) 连边的点分别为 \(l_i\) 和 \(r_i\) 。
那么说明 \(\max_{j = l_i +1}^{i-1} p[j] < p_i\) 且 \(\max_{j = i + 1}^{r_i - 1} p_j <p_i\)。
考虑构建笛卡尔树的过程,则新加入 \(i\) 节点时,\(i\) 一定为\(l_i\)的右子节点。
而在加入 \(r_i\) 时,有两种情况,\(r_i\) 要么是 \(i\) 的父节点,要么是 \(i\) 的祖先。
归纳一下可得,节点 \(i\) 可以到达笛卡尔树上它返祖链上的任意节点。
那么问题等价于求 \(k\) 个点的LCA ,求 \(k - 1\) 次LCA即可。
时间复杂度:\(O(m \log n)\),其中\(m = \sum k\)。
Code
#include <bits/stdc++.h>
#define int long long
#define ll long long
#define ull unsigned long long
#define inf 2e9
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N = 5e5 + 5,M = 1e6 + 5;
const int dx[4] = {0,0,1,-1},dy[4] = {1,-1,0,0};
int dep[N],pl[N][25];
int n,q,rt,p[N],son[N][2];
inline int build(){
stack<int> stk;
p[0] = inf , stk.push(0);
for(int i = 1;i <= n; i++){
int lst = 0;
while(!stk.empty() && p[stk.top()] < p[i])
lst = stk.top() , stk.pop();
son[stk.top()][1] = i , son[i][0] = lst , stk.push(i);
}
return son[0][1];
}
void dfs(int x,int fa){
pl[x][0] = fa;
dep[x] = dep[fa] + 1;
for(int i = 1;pl[x][i-1]; i++)
pl[x][i] = pl[pl[x][i-1]][i-1];
if(son[x][0]) dfs(son[x][0],x);
if(son[x][1]) dfs(son[x][1],x);
return ;
}
inline int LCA(int x,int y){
if(dep[x] < dep[y]) swap(x,y);
for(int i = 20;i >= 0; i--){
if(dep[pl[x][i]] < dep[y]) continue;
x = pl[x][i];
}
if(x == y) return x;
for(int i = 20;i >= 0; i--){
if(pl[x][i] == pl[y][i]) continue;
x = pl[x][i] , y = pl[y][i];
}
return pl[x][0];
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int tc = 1;
while(tc--){
cin >> n >> q;
for(int i = 1,x;i <= n; i++) cin >> p[i];
rt = build() , dfs(rt,0);
for(int o = 1,k,pre;o <= q; o++){
cin >> k >> pre;
for(int i = 2,x;i <= k; i++)
cin >> x , pre = LCA(pre,x);
cout << dep[pre] << "\n";
}
}
return 0;
}
P3793 由乃救爷爷
link.
这道题原来是要考 \(O(n) - O(1)\) RMQ 的,但可以用笛卡尔树过掉。
考虑建出符合大根堆的笛卡尔树,那么查询区间 \([l,r]\) 的最大值等价于在笛卡尔树上左右端点\(l , r\)第一次处于两个不同子树时的位置,直接暴力递归下去查询就是对的。
因为数据完全随机生成,那么区间\([l,r]\)的长度期望一定是\(\frac{n}{2}\)左右,那么大概率第一步时,\(l,r\)就在不同的子树内部,而且每向下递归一层,长度减半,\(l,r\)在不同子树内的概率也就会翻倍,所以可能单次会递归很多层,但总期望一定是每一次大致走 2 到 3 步,期望复杂度在\(O(n)\)级别。
Code
#include <bits/stdc++.h>
#define int long long
#define ll long long
#define ull unsigned long long
#define inf 2e9
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N = 2e7 + 5,M = 1e6 + 5;
const int dx[4] = {0,0,1,-1},dy[4] = {1,-1,0,0};
int n,m,rt,s,p[N],son[N][2];
namespace GenHelper {
unsigned z1,z2,z3,z4,b;
unsigned rand_() {
b = ((z1<<6) ^ z1)>>13;
z1 = ((z1&4294967294U)<<18) ^ b;
b = ((z2<<2) ^ z2)>>27;
z2 = ((z2&4294967288U)<<2) ^ b;
b = ((z3<<13) ^ z3)>>21;
z3 = ((z3&4294967280U)<<7) ^ b;
b = ((z4<<3) ^ z4)>>12;
z4 = ((z4&4294967168U)<<13) ^ b;
return (z1 ^ z2 ^ z3 ^ z4);
}
}
void srand(unsigned x){
using namespace GenHelper;
z1 = x , z2 = (~x) ^ 0x233333333U;
z3 = x ^ 0x1234598766U , z4 = (~x)+51;
}
int read(){
using namespace GenHelper;
int a = rand_()&32767;
int b = rand_()&32767;
return a * 32768 + b;
}
inline int build(){
stack<int> stk;
p[0] = inf , stk.push(0);
for(int i = 1;i <= n; i++){
int lst = 0;
while(!stk.empty() && p[stk.top()] < p[i])
lst = stk.top() , stk.pop();
son[stk.top()][1] = i , son[i][0] = lst , stk.push(i);
}
return son[0][1];
}
int query(int k,int l,int r){
if(l <= k && r >= k) return p[k];
if(r <= k) return query(son[k][0],l,r);
else return query(son[k][1],l,r);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int tc = 1;
while(tc--){
cin >> n >> m >> s , srand(s);
for(int i = 1;i <= n; i++) p[i] = read();
rt = build();
ull ans = 0;
for(int o = 1;o <= m; o++){
int l = read() % n + 1 , r = read() % n + 1;
if(l > r) swap(l,r);
ans = ans + query(rt,l,r);
}
cout << ans << "\n";
}
return 0;
}
P4755 Beautiful Pair
link.
先建出符合大根堆的笛卡尔树。
考虑枚举最大值出现的位置 \(k\) 。
那么接下来就是在笛卡尔树上做 dsu on tree,即枚举轻子树中的点权,统计重子树的贡献(二分) ,可以用树状数组等数据结构维护。
时间复杂度:\(O(n \log^2 n)\) (dsu on tree 一支 \(\log\) ,数据结构一支\(\log\))。
Code
#include <bits/stdc++.h>
#define int long long
#define ll long long
#define ull unsigned long long
#define inf 2e9
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N = 1e5 + 5,M = 1e6 + 5;
const int dx[4] = {0,0,1,-1},dy[4] = {1,-1,0,0};
int siz[N],hson[N],ans,cnt,tot;
int n,rt,p[N],son[N][2],a[N],dfn[N],pos[N];
struct Fenwick{
int f[N];
inline void add(int p,int v){
for(;p <= n;p += (p&(-p))) f[p] += v;
return ;
}
inline int ask(int p){
int res = 0;
for(;p;p -= (p&(-p))) res += f[p];
return res;
}
}T;
inline int build(){
stack<int> stk;
p[0] = inf , stk.push(0);
for(int i = 1;i <= n; i++){
int lst = 0;
while(!stk.empty() && p[stk.top()] < p[i])
lst = stk.top() , stk.pop();
son[stk.top()][1] = i , son[i][0] = lst , stk.push(i);
}
return son[0][1];
}
void dfs1(int x){
siz[x] = 1 , hson[x] = 0;
dfn[++cnt] = x , pos[x] = cnt;
for(int i = 0;i <= 1; i++){
if(!son[x][i]) continue;
dfs1(son[x][i]) , siz[x] += siz[son[x][i]];
if(siz[son[x][i]] > siz[hson[x]]) hson[x] = son[x][i];
}
return ;
}
void clear(int x){
for(int i = pos[x];i <= pos[x] + siz[x] - 1; i++)
T.add(p[dfn[i]],-1);
return ;
}
void dfs2(int x){
if(!x) return ;
int hs = hson[x] , os = son[x][0] ^ son[x][1] ^ hson[x];
dfs2(os) , clear(os) , dfs2(hs);
for(int i = pos[os];i <= pos[os] + siz[os] - 1; i++){
int d = upper_bound(a + 1,a + tot + 1,a[p[x]] / a[p[dfn[i]]]) - a - 1;
if(d > 0) ans = ans + T.ask(d);
}
for(int i = pos[os];i <= pos[os] + siz[os] - 1; i++) T.add(p[dfn[i]],1);
T.add(p[x],1);
int d = upper_bound(a + 1,a + tot + 1,1) - a - 1;
if(d > 0) ans += T.ask(d);
return ;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int tc = 1;
while(tc--){
cin >> n;
for(int i = 1;i <= n; i++) cin >> p[i] , a[i] = p[i];
sort(a + 1,a + n + 1);
tot=unique(a + 1,a + n + 1) - (a + 1);
for(int i = 1;i <= n; i++)
p[i] = lower_bound(a + 1,a + tot + 1,p[i]) - a;
rt = build() , dfs1(rt) , dfs2(rt);
cout << ans << "\n";
}
return 0;
}
P7988 [USACO21DEC] HILO G
link.
P9084 [PA 2018] Skwarki
link.
P6453 [COCI 2008/2009 #4] PERIODNI
link.
[AGC026D] Histogram Coloring
link.
P11411 兰奇的卡牌游戏
link.

浙公网安备 33010602011771号