「笔记」笛卡尔树

写在前面

貌似是一个比较冷门的算法?还是说我见识少?

不过不管怎样,既然 CCF 的 NOI 大纲里出了,就把这个知识点补上吧。

原理

笛卡尔树是一个二叉树,每一个结点有一个键值二元组 \((k,w)\) 组成(就是一个点同时带着两个信息,一般好像是下标 \(k\) 和权值 \(w\)
它要求 \(k\) 满足二叉搜索树的性质,\(w\) 满足堆的性质。

举个 OI-Wiki 上的例子

如果你看到这段文字,说明图床挂了

这张图把序列的下标当做 \(k\),元素的值当做 \(w\)

不难发现,每个元素所对应的下标在树上满足二叉搜索树的性质;
每个元素的权值也恰好构成了一个小根堆。

构建

一种常见的方法是栈构建。

首先按键值 \(k\) 排序(上面那课笛卡尔树的键值是下标,所以不用排序)。
用单调栈维护这个树的右链。
每次插入 \(u\) 执行这样一个过程:
从下到上遍历,比较键值 \(w\) 的大小,找到一个结点 \(x\) ,满足 \(x_w < u_w\),然后把 \(u\) 接到 \(x\) 的右儿子上,\(x\) 原本的右子树变为 \(x\) 的左子树。

再给一张 OI-Wiki 的图,红色框内是我们维护的右链,应该比较清晰了。

如果你看到这段文字,说明图床挂了

图中每个点进出栈最多一次,所以总的复杂度 \(O(n)\)

实现代码:

// a是给定的数组,stc是栈,sc是栈顶,ls和rs分别表示左儿子和右儿子
for(int i = 1; i <= n; ++i) {
    a[i] = read();
    int k = sc; // 用栈维护右链,构建方法有点像虚树 
    while(k && a[stc[k]] > a[i]) k--; // 找到一个小于当前点的权值的点 
    if(k) rs[stc[k]] = i; // 让 i 成为这个点的右儿子 
    if(k < sc) ls[i] = stc[k + 1]; // 让这个点上挂着的点成为 i 的左儿子 
    stc[++k] = i; // 入栈 
    sc = k; // 更新栈顶 
}

例题

P5854 【模板】笛卡尔树

题面

直接建树,然后按要求求出答案就行

只放一个主函数,前面啥也没有

signed main()
{
    n = read();
    for(int i = 1; i <= n; ++i) {
        a[i] = read();
        int k = sc; // 用栈维护右链,构建方法有点像虚树 
        while(k && a[stc[k]] > a[i]) k--; // 找到一个小于当前点的权值的点 
        if(k) rs[stc[k]] = i; // 让 i 成为这个点的右儿子 
        if(k < sc) ls[i] = stc[k + 1]; // 让这个点上挂着的点成为 i 的左儿子 
        stc[++k] = i; // 入栈 
        sc = k; // 更新栈顶 
    }
    for(int i = 1; i <= n; ++i) ans1 ^= (i * ls[i] + i), ans2 ^= (i * rs[i] + i);
    printf("%lld %lld", ans1, ans2);
    return 0;
}

P1377 [TJOI2011]树的序

题面

题意是给出一个二叉搜索树的构建顺序,要求输出它的先序遍历。

朴素的建树方法最坏情况下会被卡成 \(O(n^2)\)

这里考虑一种笛卡尔树的做法。

把给定序列的值看做下标 \(k\),下标看做权值 \(w\) 然后就可以 \(O(n)\) 建树了

为什么?

值要满足二叉查找树的限制,插入顺序上儿子比父亲晚插入,满足堆得性质。

因为给定的序列一定是 \(1 \sim n\) 的一个序列,又因为它满足二叉搜索树,那么和我们笛卡尔树中的满足二叉搜索树的性质相同。一个元素越在序列后边,它在二叉搜索树中的位置越深,换句话说,对于一个数 \(x\) ,在它到根的路径上的点一定在序列的前面。所以可以把第几个插入看做权值 \(w\) 去构建笛卡尔树。

至于先序遍历,按“根左右”的顺序输出即可。

代码还是只给出重要部分:

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

int main()
{
    n = read();
    for(int i = 1; i <= n; ++i) a[read()] = i;
    for(int i = 1; i <= n; ++i) {
        int k = sc;
        while(k && a[stc[k]] > a[i]) --k;
        if(k) rs[stc[k]] = i;
        if(k < sc) ls[i] = stc[k + 1];
        stc[++k] = i;
        sc = k;
    }
    Print(stc[1]);
    return 0;
}

P3246 [HNOI2016]序列

题面

发现有很多用莫队和 RMQ 做的,不过这里我们只讲笛卡尔树做法

\(f_{l, r}\) 表示 \(\sum_{i = l}^{r}\min_{l\le j\le i} \{a_j\}\) 的答案

\(pre_i\) 表示 \((pre_i,i]\) 这段区间内的最小值都是 \(a_i\),那么显然

\[f_{l,r} = f_{l,pre_r} + a_r \times (r - pre_r) \]

发现这玩意与 \(l\) 无关,删掉第一维即可。

显然 \(f_r = a_r \times (r - pre_r) + a_{pre_r} \times (pre_r - pre_{pre_r}) ...\)

\(f_r = \sum_{i = 1}^r \min_{i\le j \le r} \{a_j\}\)

设我们求区间 \([l,r]\),区间最小值为 \(a_p\)

考虑左在 \((p,r]\),右端点在 \(r\) 时的情况,

因为最终一定会有一个点 \(x = pre_i\)
所以 \(f_r = a_r \times (r - pre_r) + a_{pre_r} \times (pre_r - pre_{pre_r}) + ... + f_p\)

答案就是 \(f_r - f_p\)

对于所有右端点在 \((p,r]\) 中的情况类似。

所以可以设 \(g_r = \sum_{i=1}^{r}f_i\)

那么左右端点都在 \((p,r]\) 中时的答案为 \(g_r - g_p - f_p \times (r-p+1)\)

左右端点在 \([l,p)\) 时用相同的方法处理即可。

对于点 \(p\) 可以利用笛卡尔树快速找到。

剩下的看代码

/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define int long long
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;

int n, m, rt;
int a[MAXN];
int ls[MAXN], rs[MAXN];
int stc[MAXN], sc = 0;
int pre[MAXN], suf[MAXN];
int fl[MAXN], fr[MAXN], gl[MAXN], gr[MAXN];

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

int Query(int l, int r) { // 找到区间最小点 
    int x = rt;
    while(true) {
        if(l <= x && x <= r) return x; // 先遍历到的在区间内的一定是最小点 
        x = (x > r ? ls[x] : rs[x]); // 要么在区间右边,要么在区间左边 
    }
}

int calc(int l, int r, int p) { // 计算答案 
//    return gr[r] - gr[l-1] - fr[p] * (r - l + 1);
    return (p - l + 1) * (r - p + 1) * a[p] + gr[r] - gr[p] - fr[p] * (r - p) + gl[l] - gl[p] - fl[p] * (p - l);
}

signed main()
{
    n = read(), m = read();
    for(int i = 1; i <= n; ++i) {
        a[i] = read();
        int k = sc;
        while(k && a[stc[k]] > a[i]) k--; // 建立笛卡尔树 
        if(k) rs[stc[k]] = i;
        if(k < sc) ls[i] = stc[k + 1];
        stc[++k] = i;
        sc = k;
    }
    
    rt = stc[1];
    sc = 0;
    for(int i = 1; i <= n; ++i) {
        while(sc && a[stc[sc]] > a[i]) sc--; // 维护两次单调栈,让 [pre[i], i] 这个区间的最小值都是 a[i] 
        pre[i] = stc[sc], stc[++sc] = i;
    }
    for(int i = 1; i <= n; ++i) {
        fr[i] = fr[pre[i]] + a[i] * (i - pre[i]); // 利用推出来的式子进行预处理 
        gr[i] = gr[i - 1] + fr[i];
    }
//    for(int i = 1; i <= n; ++i) cout<<pre[i]<<" ";
//    cout<<"\n";
    
    sc = 0;
    for(int i = n; i >= 1; --i) {
        while(sc && a[stc[sc]] > a[i]) sc--;
        suf[i] = stc[sc], stc[++sc] = i;
    }
    for(int i = n; i >= 1; --i) {
        fl[i] = fl[suf[i]] + a[i] * (suf[i] - i);
        gl[i] = gl[i + 1] + fl[i];
    }
//    for(int i = 1; i <= n; ++i) cout<<suf[i]<<" ";
//    cout<<"\n";
    
    for(int i = 1, l, r, p; i <= m; ++i) {
        l = read(), r = read();
        p = Query(l, r);
        printf("%lld\n", calc(l, r, p));
    }
    return 0;
}
posted @ 2021-05-19 18:10  Suzt_ilymtics  阅读(193)  评论(0编辑  收藏  举报