题解:洛谷 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 \sim n\) 的排列 \(p\),需要构建笛卡尔树——满足中序遍历为原序列(BST 性质)且权值满足小根堆性质的二叉树。即对于节点 \(i\),左子树所有节点下标 \(< i\),右子树所有节点下标 \(> i\)(BST),且父节点权值 \(<\) 子节点权值(小根堆)。关键观察是:可以利用单调栈在 \(O(n)\) 时间内在线构建笛卡尔树。
-
算法选择:
- 单调栈在线构建:维护一个权值单调递增的栈,栈中存放节点下标。遍历序列时,通过出栈和入栈操作确定父子关系
-
关键步骤:
- 初始化:空栈,
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] = i,top = pos
- 记录当前栈顶位置
- 输出:计算 \(\bigoplus_{i=1}^{n} i \times (l_i + 1)\) 和 \(\bigoplus_{i=1}^{n} i \times (r_i + 1)\)
- 初始化:空栈,
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个元素最多入栈出栈各一次
- 空间复杂度:\(O(n)\),栈和树数组
-
单调栈构建笛卡尔树的核心思想:
- 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
浙公网安备 33010602011771号