zoukankan      html  css  js  c++  java
  • 「笔记」笛卡尔树

    写在前面

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

    不过不管怎样,既然 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_{lle jle i} {a_j}) 的答案

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

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

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

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

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

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

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

    因为最终一定会有一个点 (x = pre_i)
    所以 (f_r = a_r imes (r - pre_r) + a_{pre_r} imes (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 imes (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<<"
    ";
        
        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<<"
    ";
        
        for(int i = 1, l, r, p; i <= m; ++i) {
            l = read(), r = read();
            p = Query(l, r);
            printf("%lld
    ", calc(l, r, p));
        }
        return 0;
    }
    
  • 相关阅读:
    wxpython笔记:应用骨架
    go 优雅的检查channel关闭
    Golang并发模型:流水线模型
    go http数据转发
    go 互斥锁与读写锁
    go 工作池配合消息队列
    实现Tcp服务器需要考虑哪些方面
    go Goroutine泄露
    关于个人博客转移的那些事
    Java并发编程:Thread类的使用介绍
  • 原文地址:https://www.cnblogs.com/Silymtics/p/14786271.html
Copyright © 2011-2022 走看看