笛卡尔树

\(\texttt{0x00}\):前置芝士

  1. 二叉搜索树
  2. 单调栈

\(\texttt{0x01}\):概念

笛卡尔树是一种二叉树,每一个结点由一个键值二元组 \((k,w)\) 构成。要求 \(k\) 满足二叉搜索树的性质(左小右大),而 \(w\) 满足堆的性质(大根堆或小根堆)。

q1:这么一看,Treap 不也是笛卡尔树?

a1:正确的。

一个有趣的事实是,如果笛卡尔树的 \(k,w\) 键值确定,且 \(k\) 互不相同,\(w\) 互不相同,那么这个笛卡尔树的结构是唯一的。

q2:那它有什么用,写个 Treap 不是就可以代替它了吗?

a2:它在一些问题上(比如求最大值)的速度比 Treap 快,这就足够了。

\(\texttt{0x02}\):构建

对于一个 \(1\sim n\) 的排列,以下标为键值 \(k\) (满足二叉搜索树性质),数值为键值 \(w\) (满足小根堆的性质),如何建立笛卡尔树?

考虑到笛卡尔树的性质,我们先将整个序列的最小值找出来作为根节点,然后再分别在其左右区间中找到最小值作为左右子树的根节点,依次这样递归下去即可完成建树。

比如一个序列 \(\{3,5,7,1,4,2,6\}\) 它的建树过程如下:

先找到全局最小值 \(1\) 作为根节点。

递归左子树,找到区间 \(\{3,5,7\}\) 中的最小值 \(3\) 作为左子树的根节点。

递归右子树,找到区间 \(\{4,2,6\}\) 中的最小值 \(2\) 作为右子树的根节点。

继续向下递归,找到区间 \(\{5,7\}\) 中的最小值 \(5\) 作为子树根节点。

右子树中左为 \(4\),右为 \(6\)

\(7\) 挂在 \(5\) 右子树上。

最坏情况下时间复杂度为 \(O(n^2)\),用线段树或 ST 表优化后为 \(O(n\log n)\)

当然也可以用 Treap 一个一个插入,也是 \(O(n\log n)\)

P5854 【模板】笛卡尔树

注意数据范围:\(n\le 10^7\)

Treap 一下就释怀地似了。

这要求我们在线性时间复杂度内完成建树操作,怎么办?

回想一下笛卡尔树的性质,发现下标一定是单调递增的,所以我们只会在右子树上插入新的节点!

考虑一下每个点插入时的情形:

当我们插入一个点 \(i\) 时,实际上就是在右链上找到一个一个位置,使得 \(w_u\) < \(w_i\) < \(w_v\),然后我们将 \(u\) 的右儿子修改成 \(i\)\(i\) 的左儿子修改为 \(v\)

这启示我们只需要维护这个右链,显然可以用单调栈维护。

若新加的点小于栈顶,则弹出,直到栈为空或栈顶比它大。

注意这里要特判一下,若栈已经为空了,说明当前点要作为根节点,所以要把根节点更新,并且不用将栈顶的右儿子修改为 \(i\)

用插入的方式理解一下上述例子:

其中蓝色框起来的部分就是维护的右链。

\(\texttt{Code}\)

#include <iostream>
using namespace std;
const int N = 10000010;
int n;
int a[N];
int stk[N], top;
struct node{
    int ls, rs;
}tr[N];
int root;

inline int read() {
    int x = 0;
    char ch = getchar();
    while(ch < '0' || ch > '9') ch = getchar();
    while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + ch - 48; ch = getchar();}
    return x;
}

void build() {
    for(int i = 1; i <= n; i++) {
        int las = 0;
        while(top > 0 && a[stk[top]] >= a[i]) las = stk[top--];
        if(!top) root = i;
        if(top) tr[stk[top]].rs = i;
        tr[i].ls = las;
        stk[++top] = i;
    }
}

int main() {
    n = read();
    for(int i = 1; i <= n; i++) a[i] = read();
    build();
    long long res1 = 0, res2 = 0;
    for(int i = 1; i <= n; i++)
        res1 ^= 1ll * i * (tr[i].ls + 1), res2 ^= 1ll * i * (tr[i].rs + 1);
    printf("%lld %lld\n", res1, res2);
    return 0;
}

P1377 [TJOI2011] 树的序

题目大意:

根据生成序列建出一棵笛卡尔树,求一个字典序最小且和它能得到相同笛卡尔树的生成序列。

思路:

先解释一下为什么是建出一颗笛卡尔树。

首先键值 \(k\) 满足 BST 性质,其次它是一个一个按顺序插入到子树里的,要保证构造出的二叉搜索树和原来一样,所以每次插入时,根是唯一确定的(因为二叉搜索树不像平衡树可以旋转,根节点必须先插入),如果建立一个时间戳,父节点的时间戳一定小于子节点,这就满足小根堆性质,可以把时间戳当成笛卡尔树中的第二个键值,所以是建一棵笛卡尔树。

但是左右儿子的插入顺序是可以交换的

所以接下来思路就很明显了,我们先建一棵小根堆笛卡尔树。

由于要字典序最小,所以我们得把尽量小的点往左子树里塞,对应到笛卡尔树中的具体操作就是对该笛卡尔树进行先序遍历,直接输出遍历到的每个点即可。

只需要在模板题的代码上加一个 dfs 就行了。

时间复杂度为 \(O(n)\)

\(\texttt{Code}\)

#include <iostream>

using namespace std;

const int N = 100010;

int n;
int a[N];
int stk[N], top;
struct node{
    int ls, rs;
}tr[N];
int root;

inline int read() {
    int x = 0;
    char ch = getchar();
    while(ch < '0' || ch > '9') ch = getchar();
    while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + ch - 48; ch = getchar();}
    return x;
}

void dfs(int u) {
    printf("%d ", u);
    if(tr[u].ls) dfs(tr[u].ls);
    if(tr[u].rs) dfs(tr[u].rs);
}

void build() {
    for(int i = 1; i <= n; i++) {
        int las = 0;
        while(top > 0 && a[stk[top]] >= a[i]) las = stk[top--];
        if(!top) root = i;
        if(top) tr[stk[top]].rs = i;
        tr[i].ls = las;
        stk[++top] = i;
    }
    dfs(root);
}

int main() {
    n = read();
    for(int i = 1; i <= n; i++) a[read()] = i;
    build();
    return 0;
}
posted @ 2024-07-23 15:10  Brilliant11001  阅读(12)  评论(0编辑  收藏  举报