洛谷 P1440:求m区间内的最小值 ← 笛卡尔树

【题目来源】
https://www.luogu.com.cn/problem/P1440

【题目描述】
一个含有 n 项的数列,求出每一项前的 m 个数到它这个区间内的最小值。若前面的数不足 m 项则从第 1 个数开始,若前面没有数则输出 0。

【输入格式】
第一行两个整数,分别表示 n,m。
第二行,n 个正整数,为所给定的数列 ai。

【输出格式】
n 行,每行一个整数,第 i 个数为序列中 ai 之前 m 个数的最小值。

【输入样例】
6 2
7 8 1 4 3 2

【输出样例】
0
7
7
1
1
3

【说明/提示】
对于 100% 的数据,保证 1≤m≤n≤2×10^6,1≤ai≤3×10^7。

【算法分析】
如何构建笛卡尔树?请参考:https://blog.csdn.net/hnjzsyjyj/article/details/138400888
● 笛卡尔树结构由 Vuillmin(1980) 在解决范围搜索的几何数据结构问题时提出。笛卡尔树是一种非常特殊的二叉查找树(BST)。每个结点有两个信息 (pri, val),如果只考虑 pri,它是一棵二叉查找树,如果只考虑 val,它是一个小根堆。其中,pri 是结点插入顺序,或称优先级。val 是结点的值。也就是说,笛卡尔树具有二叉查找树和堆的双重特性。

简言之,一棵笛卡尔树,val 决定了结点的值,pri 决定了结点的位置。

● 如何构建笛卡尔树?(参考于:https://wansuanfa.com/index.php/776
笛卡尔树的构建步骤如下:(构建前需对 pri 递增排序,或选择数组元素的默认递增下标)
(1)用数组中的第一个元素创建根结点。
(2)遍历数组中剩下的元素,如果新元素比根结点小,让根结点成为新结点的左子结点,然后新结点成为根结点。
(3)如果新结点比根结点大,就从根结点沿着最右边这条路径一种往右走,直到找到比新结点大的结点 node ,也可能没找到。如果找到,就让新结点替换 node 结点的位置,让 node 结点变成新结点的左子结点。如果没找到,就让新结点成为最后查找的那个结点的右子结点。
(4)重复上面步骤(2),(3),直到元素全部遍历完。

● 构建笛卡尔树的关键点解析(参考于:https://wansuanfa.com/index.php/776
(1)如果新结点比根结点小,根据小根堆(元素的值满足小根堆)的性质,那么新结点一定是根结点的父节点。又因为根结点的下标比新结点小,根据二叉查找树(元素的下标满足二叉查找树)的性质,根结点只能是新结点的左子树。
(2)如果新结点比根结点大,那么新结点只能往右边查找,不能往左边,如果往左边就不满足二叉查找树的性质了。如果比右子结点还大,就继续往右,所以只要比右子结点大就会一直往右。如果比右子结点小,根据小根堆的性质,新结点就会成为这个右子结点的父结点,根据二叉查找树的性质,这个右子结点要成为新结点的左子结点(因为右子结点的下标比新结点小)。

下图为构建笛卡尔树的过程示意图。通过构建过程,易知如何维护笛卡尔树每个结点的两个信息
(pri, val),以及如何保障笛卡尔树具有二叉查找树和堆的双重特性。由图可知,构建方法为以 val 值作为结点,并用其构建小根堆。当需要调整时,以结点插入顺序(或称优先级 pri)为依据,将对应结点置于左子树或右子树,从而保证“如果只考虑 pri,它是一棵二叉查找树”的特性。
简言之,一棵笛卡尔树,val 决定了结点的值,pri 决定了结点的位置。

● 单调栈:https://blog.csdn.net/hnjzsyjyj/article/details/117370314
单调栈是一种非常适合处理“下一个更大元素(Next Greater Number)”问题的数据结构。
笛卡尔树的构建过程中,常还需借助“
单调栈”这种数据结构,以保证在线性时间内完成笛卡尔树的构建。单调栈中保存的始终是笛卡尔树的右链,即由笛卡尔树的“根结点、根结点的右儿子、根结点的右儿子的右儿子、……”组成的链。并且,为了维护笛卡尔树的小根堆特性,单调栈中的元素值从栈顶到栈底需依次递减,且栈底存储的是根结点。由于每个元素最多进出站各一次,所以时间复杂度为 O(n)。

● 快读:https://blog.csdn.net/hnjzsyjyj/article/details/120131534
在数据量比较大的时候,即使使用 scanf 函数读入数据也会超时,这时就需要使用“快读”函数了。“
快读”函数之所以快,是因为其中的 getchar 函数要比 scanf 函数快。 网传,“快读”函数能以比 scanf 函数快5倍的速度读入任意整数
貌似,笛卡尔树问题常用到快读。
快读代码如下:

int read() { //fast read
    int x=0,f=1;
    char c=getchar();
    while(c<'0' || c>'9') { //!isdigit(c)
        if(c=='-') f=-1;
        c=getchar();
    }
    while(c>='0' && c<='9') { //isdigit(c)
        x=x*10+c-'0';
        c=getchar();
    }
    return x*f;
}

【算法代码】

#include <bits/stdc++.h>
using namespace std;

const int maxn=2e6+5;
int a[maxn];
int pre[maxn],ls[maxn],rs[maxn];
int n,m;

int buildCartesianTree() {
    stack<int> s;
    int root,last;
    for(int i=1; i<=n; i++) {
        last=0;
        while(!s.empty()) {
            if(a[s.top()]<a[i]) {
                root=s.top();
                if(rs[root]) {
                    pre[rs[root]]=i;
                    ls[i]=rs[root];
                }
                rs[root]=i;
                pre[i]=root;
                break;
            }
            last=s.top();
            s.pop();
        }
        if(s.empty() && last) {
            pre[last]=i;
            ls[i]=last;
        }
        s.push(i);
    }
    while(!s.empty()) {
        root=s.top();
        s.pop();
    }
    return root;
}

int query(int root,int le,int ri) {
    while(root<le || root>ri)
        root=root<le?rs[root]:ls[root];
    return root;
}

int main() {
    scanf("%d %d",&n,&m);
    for(int i=1; i<=n; i++) scanf("%d",&a[i]);

    int root=buildCartesianTree();

    for(int i=1; i<=n; i++) {
        int le,ri;
        ri=i-1;
        le=ri-m+1;
        if(ri<1) {
            printf("0\n");
            continue;
        } else {
            if(le<1) le=1;
        }
        printf("%d\n",a[query(root,le,ri)]);
    }

    return 0;
}

/*
in:
6 2
7 8 1 4 3 2

out:
0
7
7
1
1
3
*/



【参考文献】
https://blog.csdn.net/shamansi99/article/details/100049680
https://www.luogu.com.cn/problem/solution/P1440



 

  • 28
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值