题解:洛谷 P5854 笛卡尔树

【题目来源】

洛谷:P5854 【模板】笛卡尔树 - 洛谷

【题目描述】

给定一个\(1\sim n\)的排列数组\(p\),构建其笛卡尔树。即构建一棵二叉树,满足:

(1)每个节点的编号满足二叉搜索树的性质。

(2)节点\(i\)的权值为\(p[i]\),每个节点的权值满足小根堆的性质。

【输入】

第一行一个整数\(n\)

第二行一个排列\(p_{1...n}\)

【输出】

\(l_i\)\(r_i\)分别表示节点\(i\)的左右儿子的编号(若不存在则为\(0\))。

输出一行两个整数,分别表示\(xor_{i=1}^ni\times(l_i+1)\)\(xor_{i=1}^ni\times(r_i+1)\)

【输入样例】

5
4 1 3 2 5

【输出样例】

19 21

【核心思想】

  1. 问题分析:给定 \(1 \sim n\) 的排列 \(p\),需要构建笛卡尔树——满足中序遍历为原序列(BST 性质)且权值满足小根堆性质的二叉树。即对于节点 \(i\),左子树所有节点下标 \(< i\),右子树所有节点下标 \(> i\)(BST),且父节点权值 \(<\) 子节点权值(小根堆)。关键观察是:可以利用单调栈\(O(n)\) 时间内在线构建笛卡尔树。

  2. 算法选择

    • 单调栈在线构建:维护一个权值单调递增的栈,栈中存放节点下标。遍历序列时,通过出栈和入栈操作确定父子关系
  3. 关键步骤

    • 初始化:空栈,top = 0
    • 遍历每个位置 \(i\)\(1\)\(n\)
      • 记录当前栈顶位置 pos = top
      • 维护单调性while (p[stk[pos]] > p[i]) pos--
        • 弹出权值大于 \(p[i]\) 的栈顶元素,这些元素将成为 \(i\) 的左子树候选
      • 建立左儿子关系:若 pos < top(有元素被弹出),则 t[i].ls = stk[pos + 1]
        • 最后弹出的元素(即第一个不大于 \(p[i]\) 的元素之后的那个)成为 \(i\) 的左儿子
      • 建立右儿子关系:若 pos > 0(栈非空),则 t[stk[pos]].rs = i
        • 当前栈顶元素(第一个不大于 \(p[i]\) 的元素)的右儿子为 \(i\)
      • 入栈stk[++pos] = itop = pos
    • 输出:计算 \(\bigoplus_{i=1}^{n} i \times (l_i + 1)\)\(\bigoplus_{i=1}^{n} i \times (r_i + 1)\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个元素最多入栈出栈各一次
    • 空间复杂度:\(O(n)\),栈和树数组
  5. 单调栈构建笛卡尔树的核心思想

    • BST 与中序遍历的对应:由于中序遍历必须是 \(1, 2, \ldots, n\),新元素 \(i\) 只能作为之前某元素的右儿子或后续元素的左儿子。单调栈维护了当前右链(从根到最右节点的路径)
    • 小根堆性质的维护:栈单调递增确保栈顶权值最小。当 \(p[i]\) 更小时,弹出更大的元素,这些被弹出的元素构成 \(i\) 的左子树(因为它们下标 \(< i\) 且权值 \(> p[i]\)),而 \(i\) 成为当前右链的新末端
    • 左儿子与右儿子的区分:被弹出的元素中,最后一个弹出的是 \(i\) 的左儿子(下标最接近 \(i\) 且权值 \(> p[i]\));栈顶元素(未弹出的最大者)的右儿子是 \(i\)(因为 \(i\) 下标更大且权值更小,满足 BST 和堆性质)
    • 线性构建的优雅性:无需递归或显式建树,通过栈的一次遍历即可确定所有父子关系,是单调栈在树构造中的经典应用
    • 适用于 RMQ 问题的 \(O(1)\) 查询结构、笛卡尔树相关性质分析、以及需要同时满足 BST 和堆性质的树构造场景

【算法标签】

普及+ #笛卡尔树

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 10000005;
int n, p[N];
long long sum1, sum2;
struct DescarTree
{
    int ls, rs;  // 左右儿子编号
} t[N];  // 笛卡尔树
int stk[N], top;  // stk[]单调递增栈,存放元素下标,增栈指权值,栈顶元素权值大
int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);  // 加速IO
    cin >> n;  // 输入数组长度
    for (int i = 1; i <= n; i++)  // 输入数组并构建笛卡尔树
    {
        cin >> p[i];
        int pos = top;  // top是原栈顶,pos是栈当前操作位置
        // 维护递增栈:出栈直到栈顶元素<=当前元素
        while (p[stk[pos]] > p[i])
        {
            pos--;
        }
        if (pos < top)  // 如果有出栈元素
        {
            t[i].ls = stk[pos + 1];  // 最后出栈的元素是当前节点的左儿子
        }
        if (pos)  // 如果栈非空
        {
            t[stk[pos]].rs = i;  // 当前节点是栈顶元素的右儿子
        }
        stk[++pos] = i;  // 当前元素入栈
        top = pos;  // 更新栈顶
    }
    for (int i = 1; i <= n; i++)  // 计算结果
    {
        sum1 ^= 1LL * i * (t[i].ls + 1);
        sum2 ^= 1LL * i * (t[i].rs + 1);
    }
    cout << sum1 << " " << sum2 << endl;  // 输出结果
    return 0;
}

【运行结果】

5
4 1 3 2 5
19 21
posted @ 2026-07-02 22:44  团爸讲算法  阅读(12)  评论(0)    收藏  举报