笛卡尔树
本质
笛卡尔树,就是一棵"递归地用最小值去划分区间"的二叉树。
给定一个序列,不断做这样一件事:
- 找到整个序列的最小值 ,把它当作根;
- 最小值左边 的那段子序列,递归建成左子树;
- 最小值右边 的那段子序列,递归建成右子树。
这样得到的树,就是笛卡尔树。它把"区间最小值"这种信息,用树的结构显式地表达了出来
递归定义
设序列为 \(a1\\dots n\)(允许元素有重复),定义它的笛卡尔树:
- 若序列为空,对应空树;
- 否则,设 \(m\) 是 \(a\) 中最小值 首次出现的位置
- 根节点对应位置 \(m\),权值为 \(am\);
- 左子树 = \(a1\\dots m-1\) 的笛卡尔树;
- 右子树 = \(am+1\\dots n\) 的笛卡尔树。
例 :\(a = 4, 1, 3, 2, 5\)(下标 \(1\) 到 \(5\))
- 全局最小值是 \(a2=1\),根是下标 \(2\);
- 左半段 \(4\)(下标 \(1\))建成左子树;
- 右半段 \(3,2,5\) 的最小值是 \(a4=2\),作为右子树根,其左 \(3\)、右 \(5\) 分别是它的左右儿子。
cpp
2(值=1) ← 全局最小值是根
/ \
1(值=4) 4(值=2) ← 右半段 [3,2,5] 的最小值是 2
/ \
3(值=3) 5(值=5)
从递归定义立刻能看出两个重要事实:
- 中序遍历(左 - 根 - 右)恰好就是原序列顺序
- 根是全局最小值;每个节点的权值,都 ≤ 它整棵子树里的所有权值------这正是"小根堆"性质。
二叉搜索树 + 堆
递归定义很直观,但更常用下面的等价定义
笛卡尔树的每个节点带两个键:
| 键 | 含义 | 满足的性质 |
|---|---|---|
| key(键) | 通常取为数组下标 \(i\) | 满足二叉搜索树性质:左子树所有 key < 根,右子树所有 key > 根 |
| value(权值) | 取为数组元素 \(ai\) | 满足小根堆性质:根的 value ≤ 子树内所有 value |
即:中序遍历是下标的升序,权值自根向下单调不降(小根堆)。
这两条性质和上一节的递归定义是完全等价的:
- "key 满足 BST" \(\Longleftrightarrow\) 中序遍历就是下标顺序 \(1,2,\dots,n\);
- "value 满足小根堆" \(\Longleftrightarrow\) 每个节点是它子树(一段连续区间)的最小值。
因此,一棵笛卡尔树同时是"按位置排好序"和"按大小分好层"的一棵树。
性质
性质 1:中序遍历 = 原序列
性质 2:唯一性
性质 3:子树 = 一段连续区间
性质 4:节点是它子树区间的最小值
性质 5:区间最小值 = LCA
对任意区间 \(l, r\)(\(l \le r\)),区间最小值的位置 = 节点 \(l\) 与节点 \(r\) 在笛卡尔树上的 LCA(最近公共祖先)。
这个性质把 RMQ问题 和 LCA 打通了,二者互为桥梁。
单调栈 O(n) 建树
递归定义需要每次找"区间最小值",如果每次都扫描是 \(O(n^2)\) 的。但我们可以用单调栈在线性时间内一次建好整棵树。
核心思想
从左到右逐个加入 下标 \(i\)。始终维护一个栈,栈里存的是"当前已建成的树的右链 "------也就是从根一路往右走的那条路径。并且这条右链上的权值是严格递增的(小根堆的要求:越靠根权值越小)。
加入新节点 \(i\) 时:
- 弹栈:把栈顶权值 > \(ai\) 的节点全部弹出(它们比 \(i\) 大,不该待在 \(i\) 的上面),记最后一个弹出的节点为
last; - 挂右儿子:若栈非空,说明栈顶是第一个权值 ≤ \(ai\) 的节点,令 \(i\) 成为它的右儿子 ,显然,这一步满足\(MST\) 性质;
- 挂 \(i\) 的左儿子:被弹出的那些节点(连同它们的子树)整体比 \(i\) 大,成为 \(i\) 的左儿子 ,即令
last成为 \(i\) 的左儿子,显然,这一步也是满足 \(MST\) 性质的,因为这些节点都在 \(i\)之前被访问,且刚好连接; - 入栈:把 \(i\) 压入栈顶(继续维护右链)。
处理完所有下标后,栈底元素就是全局最小值,也就是根。
正确性
关键:每个节点只会"弹栈"一次,也只会"入栈"一次 ,所以总复杂度是 \(O(n)\)。
模板
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e7 + 5;
ll n;
ll p[N];
ll ls[N], rs[N];
ll stk[N], top;
ll root;
void build() {
top = 0;
for (int i = 1; i <= n; i ++) {
ll lst = 0;
while (top && p[stk[top]] > p[i]) lst = stk[top --];
if (top) rs[stk[top]] = i;
if (lst) ls[i] = lst;
stk[++ top] = i;
}
root = stk[1];
}
int main () {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i ++) cin >> p[i];
build();
ll x = 0, y = 0;
for (int i = 1; i <= n; i ++) {
x = x ^ (i * (ls[i] + 1));
y = y ^ (i * (rs[i] + 1));
}
cout << x << ' ' << y << '\n';
return 0;
}
其余用途
- 字典序最小的树遍历:某些"按权值限制、按位置构造"的树上问题,可直接落到笛卡尔树的结构上处理。
- 区间最值划分 DP:形如"每次取区间最小值把区间一分为二"的分治 DP,其递归树就是笛卡尔树,可在树上做树形 DP。
注:
退化成链 :当序列单调时(如严格递增),笛卡尔树是一条右链 (或左链),深度 \(O(n)\)。此时若用递归需注意爆栈,改用非递归。
总结
- 本质:笛卡尔树 = 用"区间最小值"递归划分序列得到的树。
- 双重结构:中序 = 原序列顺序(BST),权值 = 小根堆。
- 核心性质 :区间 \(l,r\) 的最小值位置 = LCA\((l,r)\)。
- 构建 :单调栈 \(O(n)\)。
- 应用 :最大子矩形、RMQ 与 LCA 互转 、区间最小值划分类 DP。