笛卡尔树

笛卡尔树


本质

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

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

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

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


递归定义

设序列为 \(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\) 时:

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

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

正确性

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


模板

P5854 【模板】笛卡尔树

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。
相关推荐
Scintilla_y1 小时前
DAY2 滑动窗口
算法·leetcode·散列表
026d231 小时前
状态信号只知“其然”不知“其所以然”:变工况机械故障诊断的物理盲区
算法
姜鱼问生2 小时前
备份和服务器在同一台机器上,等于没备份
算法·架构
姜鱼问生2 小时前
别再 chmod 777 了,用 ACL 给它开一扇窗
后端·算法·架构
特立独行的猫a2 小时前
仓颉语言UI开发实战:我用仓颉写了个桌面版力扣
算法·leetcode·ui·tauri·仓颉·cj-tarui
y1su2 小时前
Leetcode 动态规划 模板
java·数据结构·算法·leetcode·动态规划
All for pursuit.3 小时前
【位运算-3】136.只出现一次的数字
数据结构·c++·算法·leetcode
MoRanzhi12033 小时前
106.正则问题(分治)
python·算法·正则表达式·蓝桥杯·递归·分治算法·字符串处理
suhan4 小时前
【数据结构】五、树:7.哈夫曼树、哈夫曼编码
数据结构·算法·排序算法