【训练题27:单调队列实现RMQ(附带详细说明)】 H :圣诞糖果 | 吉首大学第十届“新星杯”

H :圣诞糖果 | 吉首大学第十届“新星杯”

题外话

  • 差一题就能拿一等奖了,有点残念
  • 当时试过了线段树/树状数组/优先队列写法,但是都没过
  • 这题网上我没找到题解,估计也只能用单调队列实现吧(猜的)

难度

− 11 249 -\frac{11}{249} 24911

题意

N N N 堆礼物围成一个环,顺时针编号依次从 1 到 N N N,每堆礼物个数 A i A_i Ai
现在你要选择连续的一堆礼物,礼物个数至少是 M M M个。
选择礼物的花费为:这连续一堆礼物中数量最大值的一堆的数量 减去 这堆礼物中数量最小值的这堆的数量

正式表达:给定 1 ≤ i ≤ j ≤ N 1\le i\le j\le N 1ijN,使得 ∑ k = i j A k ≥ M \sum_{k=i}^jA_k\ge M k=ijAkM
求出 min ⁡ { max ⁡ j k = i A k − min ⁡ j k = i A k } \min\{\underset{k=i}{\overset{j}{\max}}A_k-\underset{k=i}{\overset{j}{\min}}A_k\} min{k=imaxjAkk=iminjAk}

数据范围

1 ≤ N ≤ 1 0 6 1\le N\le 10^6 1N106
A i ≤ 1 0 12 A_i\le 10^{12} Ai1012

思路

  • 易得,把 [ 1 , N ] [1,N] [1,N]的环 拓展成 [ 1 , 2 N ] [1,2N] [1,2N]的链之后,可以通过尺取来求出所有满足条件的 i 、 j i、j ij
  • 这时候,题目变成了:求下标范围为 [ i , j ] [i,j] [i,j] 中的最小值和最大值
    当时很单纯的我以为就线段树随便搞搞就行了,但是发现时间 O ( N log ⁡ N ) O(N\log N) O(NlogN)可能是够的,但是内存 128 M 128M 128M 怎么都压不下来,导致一直运行错误,现在才发现其实就是 M L E MLE MLE了。
  • 普通的线段树或者刚刚学的ST表的内存需求是 O ( N log ⁡ N ) O(N\log N) O(NlogN)(当然线段树可能因为我用的板子很挫),最普通的 O ( N ) O(N) O(N) 内存 2 e 6 ∗ 8 / 1024 / 1024 = 15 M 2e6*8/1024/1024=15M 2e68/1024/1024=15M了,带一些常数的算法都会很难通过。。

单调队列就出现了。

  • 这玩意儿可以在滑动窗口的情况下,快速算出这一段窗口的最值。有多呢?

  • 滑窗是 O ( N ) O(N) O(N)的,每一步滑窗,摊还时间复杂度:处理 O ( 1 ) O(1) O(1),查询 O ( 1 ) O(1) O(1)

  • 综合时间复杂度:严格 O ( N ) O(N) O(N),空间复杂度: O ( N ) O(N) O(N)

  • 怎么用滑窗实现 求范围内最值 呢?

    • 可以使用双端队列deque<type>,也可以使用一个简单的数组模拟。(吉首这题用STL卡常,建议使用数组模拟)。

单调队列的操作实现

【以求最小值为例】

  • 首先理解这句话:“单调队列里面存储的元素是原数组的下标”

  • 添加元素

    • 窗口向右,会添加新的元素。
    • 如果新的数字比队列更大,那肯定不要它;反过来,如果队列的比新的数字还要大,那么队列尾就去掉,直到队列空掉或者遇到一个比新数字还小的数字。
    • 接下来,把新的数字的下标加进队列
  • 删除元素

    • 因为是滑窗,肯定窗口左端有些值会离开窗口,因此我们希望删掉它。
    • 但是不能这样考虑。我们考虑:窗口左端点为 s t st st,我们需要删掉所有在窗口左端点还左边的非法元素
    • 如果队列储存的元素(即下标)小于 s t st st(即窗口左端点),那么他们是非法元素,丢掉他们。
  • 查询窗口内元素的最值:

    • 队列储存的下标所指向的元素即为最值。
  • 为什么要这么处理?

    • 其实可以这样快速理解:双端队列的存储目前的最值的下标。
    • 但是由于窗口滑动,窗口中的其他值也有可能成为某段区间的最值。
    • 我们把其他可能的元素按顺序从头到尾进行了排列,并且因为添加元素时附带删掉了一些不可能成为答案的元素。
    • 另外,删除元素的时机就是,队列也就是目前的最值已经过期(在窗口外了)然后再删掉。

这样,这题其实已经完成了。细节代码处理见下吧。

核心代码

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 2e6+50;
const ll INF = 2e18;

ll aa[MAX];
ll Q[MAX],R[MAX];								/// Q:最小值双端队列	R:最大值双端队列
int main(){
    int T;scanf("%d",&T);
    while(T--){
        int n;ll m;
        scanf("%d%lld",&n,&m);
        for(int i = 1;i <= n;++i)scanf("%lld",&aa[i]),aa[i+n]=aa[i];
        int st = 1,ed = 1;						/// 窗口端点
        int st1 = 1,ed1 = 1,st2 = 1,ed2 = 1;	/// 双端队列的端点
        ll now = aa[1];
        Q[st1] = 1;R[st1] = 1;
        ll ans = INF;
        while(1){
            while(ed + 1 <= n * 2 && now < m && ed - st + 1 + 1 <= n){		/// 添加元素处理
                ed++;
                now += aa[ed];
                while(ed1 - st1 >= 0 && aa[Q[ed1]] >= aa[ed])ed1--;
                while(ed2 - st2 >= 0 && aa[R[ed2]] <= aa[ed])ed2--;
                Q[++ed1] = ed;
                R[++ed2] = ed;
            }
            if(now < m)break;
            ans = min(ans,aa[R[st2]] - aa[Q[st1]]);			/// 查询
            now -= aa[st];
            st++;
            while(ed1 - st1 >= 0 && Q[st1] < st)st1++;		/// 删除元素处理
            while(ed2 - st2 >= 0 && R[st2] < st)st2++;
        }
        if(ans == INF)printf("-1\n");
        else printf("%lld\n",ans);
    }
    return 0;
}

补充

  • 可能不用 S T L STL STL 的代码对于初次阅读的体验比较差,这里补一份只用 S T L STL STL 的题
  • 上一篇的用 S T ST ST 表实现 R M Q RMQ RMQ 的题目,用单调队列再次实现一下:质量检测 | 洛谷 P2251

单调队列代码如下:
可以看到,直接 S T L STL STL 的代码量并不多,但是应该思考下为什么这样。

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 2e6+50;

int aa[MAX];
deque<int>Q;
int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;++i)scanf("%d",&aa[i]);
    for(int i = 1;i <= n;++i){
        while(Q.size() && i-Q.front() >= m)Q.pop_front();
        while(Q.size() && aa[Q.back()] >= aa[i])Q.pop_back();
        Q.push_back(i);
        if(i >= m)printf("%d\n",aa[Q.front()]);
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值