文章目录

‹
›

笛卡尔树

笛卡尔树


本质

笛卡尔树,就是一棵"递归地用最小值去划分区间"的二叉树。

给定一个序列,不断做这样一件事:

  1. 找到整个序列的最小值,把它当作根;
  2. 最小值左边的那段子序列,递归建成左子树;
  3. 最小值右边的那段子序列,递归建成右子树。

这样得到的树,就是笛卡尔树。它把"区间最小值"这种信息,用树的结构显式地表达了出来


递归定义

设序列为 \(a[1\dots n]\)(允许元素有重复),定义它的笛卡尔树:

  • 若序列为空,对应空树;
  • 否则,设 \(m\) 是 \(a\) 中最小值首次出现的位置
    • 根节点对应位置 \(m\),权值为 \(a[m]\);
    • 左子树 = \(a[1\dots m-1]\) 的笛卡尔树;
    • 右子树 = \(a[m+1\dots n]\) 的笛卡尔树。

例:\(a = [4, 1, 3, 2, 5]\)(下标 \(1\) 到 \(5\))

  • 全局最小值是 \(a[2]=1\),根是下标 \(2\);
  • 左半段 \([4]\)(下标 \(1\))建成左子树;
  • 右半段 \([3,2,5]\) 的最小值是 \(a[4]=2\),作为右子树根,其左 \([3]\)、右 \([5]\) 分别是它的左右儿子。
              2(值=1)              ← 全局最小值是根
             /        \
          1(值=4)     4(值=2)      ← 右半段 [3,2,5] 的最小值是 2
                     /      \
                   3(值=3)  5(值=5)

从递归定义立刻能看出两个重要事实:

  • 中序遍历(左 - 根 - 右)恰好就是原序列顺序
  • 根是全局最小值;每个节点的权值,都 ≤ 它整棵子树里的所有权值——这正是"小根堆"性质。

二叉搜索树 + 堆

递归定义很直观,但更常用下面的等价定义

笛卡尔树的每个节点带两个键:

键 含义 满足的性质
key(键) 通常取为数组下标 \(i\) 满足二叉搜索树性质:左子树所有 key < 根,右子树所有 key > 根
value(权值) 取为数组元素 \(a[i]\) 满足小根堆性质:根的 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\) 时:

  1. 弹栈:把栈顶权值 > \(a[i]\) 的节点全部弹出(它们比 \(i\) 大,不该待在 \(i\) 的上面),记最后一个弹出的节点为 last;
  2. 挂右儿子:若栈非空,说明栈顶是第一个权值 ≤ \(a[i]\) 的节点,令 \(i\) 成为它的右儿子,显然,这一步满足\(MST\) 性质;
  3. 挂 \(i\) 的左儿子:被弹出的那些节点(连同它们的子树)整体比 \(i\) 大,成为 \(i\) 的左儿子,即令 last 成为 \(i\) 的左儿子,显然,这一步也是满足 \(MST\) 性质的,因为这些节点都在 \(i\)之前被访问,且刚好连接;
  4. 入栈:把 \(i\) 压入栈顶(继续维护右链)。

处理完所有下标后,栈底元素就是全局最小值,也就是根。

正确性

关键:每个节点只会"弹栈"一次,也只会"入栈"一次,所以总复杂度是 \(O(n)\)。


模板

P5854 【模板】笛卡尔树

#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。
posted @ 2026-10-16 19:17  lvwangshu  阅读(55)  评论(0)    收藏  举报