单调栈 单调队列

单调栈

单调栈:基于栈的数据结构,栈中数据从栈底至栈顶具有单调性,序列中每个元素都必须要进入一次单调栈。由于单调性,序列元素无需时刻都保留在单调栈内。

在这里插入图片描述

应用:求解 N G E / N L E NGE/NLE NGE/NLE P G E / P L E PGE/PLE PGE/PLE类问题(序列中下一个/上一个 更大/更小的元素),RMQ问题

  • 更大元素问题( G r e a t e r Greater Greater):构造单调递减栈;更小元素问题( L e s s Less Less):构造单调递增栈。原因在于即将入栈元素一定是要对当前栈顶元素的答案没有贡献,所以才能让其入栈。
  • 序列上一个类问题( P r e v i o u s ) Previous) Previous)即将入栈元素为主导,看栈顶元素(往前看)。修改的是即将入栈元素不允许栈顶元素与即将入栈元素相同(入栈前必须将栈中与其相同元素全部岀栈)。
    序列下一个类问题( N e x t Next Next):栈顶元素为主导,看即将入栈元素(往后看)。修改的是栈顶元素允许即将入栈元素与栈顶元素相同。
  • 总结:构造的单调性与求解问题相反,方向与求解问题相同,上一个不允许同,下一个允许同

N G E / N L E NGE/NLE NGE/NLE问题

具有相同答案元素具有连续性与单调性。暴力法的核心瓶颈在于,对于具有重复答案的部分进行了大量的反复比较。

优化思路:

  • 批量处理:具有相同答案的元素具有连续性
  • 动态维护:一旦元素确定答案,其将不可能改变答案,也不可能再成为其他元素的答案,其不再被考察
  • 单调性:具有相同答案的元素具有单调性遍历序列时动态维护单调子序列,作为待确定答案元素的集合。若新元素破坏了子序列单调性,则其对单调子序列末尾元素答案确定产生了贡献,其必定为至少一个元素的答案。这个动态维护的集合就是单调栈。

NGE/NLE问题中,单调栈用于暂存未确定答案的元素,用于这些元素的批量确定答案。一旦有比其更大/小的元素进入栈,则其答案确定,该元素立即出栈。

核心思想是栈顶元素为主导,看即将入栈元素。修改的是栈顶元素。相同元素一定不会产生答案贡献,可直接入栈。允许即将入栈元素与栈顶元素相同。确定答案为当单调性被破坏时,发生在该元素出栈时。

vector<pair<int,int>>v;//存储本身及其下标
vector<int>ans;
stack<int>s;//存储下标
void nge(){
    for(int i=0;i<v.size();i++){
        while(s.size()&&v[s.top()].first<v[i].first)//NLE为> 允许栈顶元素和即将入栈元素相同
            ans[v[s.top()].second]=i,s.pop();
        s.push(i);
    }
}

P G E / P L E PGE/PLE PGE/PLE问题

具有相同答案元素具有连续性与单调性。考虑动态维护单调子序列,由于答案位于元素前方,因此该序列是作为潜在答案的集合。若新元素破坏了单调性,则其比至少一个元素在位置(更靠右)和大小上都更优,则其更适合作为答案,将失去对答案贡献的元素移除序列。这其实就是单调栈。

PGE/PLE问题中,单调栈用于潜在答案的集合,用于为之后的元素批量确定答案,潜在答案呈单调递减性。

入栈元素:在距离上离后续元素更近,且对后续元素有潜在的答案贡献。

出栈元素:一旦有比其更优(两个维度:(1)距离 (2)元素大小)的答案入栈,其对之后的元素便失去贡献,其立即出栈。

核心思想是以即将入栈元素为主导,看栈顶元素。修改的是即将入栈元素

不允许栈顶元素与即将入栈元素相同。考虑对后续元素的影响:相同元素中最后一个距离后续元素更近,因此只有最后一个可能会成为后续元素的答案;考虑对相同元素自身的影响:只能访问栈顶元素。若相同元素保留在栈内,会导致只有第一个元素能获取到答案,其他相同元素只能访问到第一个相同元素。显然这不是其答案,会导致遗漏。

vector<pair<int,int>>v;//存储本身及其下标
vector<int>ans;
stack<int>s;//存储下标
void pge(){
    for(int i=0;i<v.size();i++){
        while(s.size()&&v[i].first>=v[s.top()].first) s.pop();PLE为<= 不允许栈顶元素与入栈元素相同
        if(s.size()) ans[v[i].second]=s.top();
        s.push(i);
    }
}

总结

下一个更大(小)元素问题上一个更大(小)元素问题
单调栈作用待确定答案的元素的集合对之后元素答案的确定存在潜在贡献集合
入栈元素待确定答案的元素该元素能对序列之后元素答案确定产生潜在贡献
出栈元素该元素答案已完成确定该元素已无法对之后元素答案确定产生潜在贡献
相同元素是否可入栈
答案确定时机元素出栈元素入栈
方向栈顶元素为主导,看即将入栈元素即将入栈元素为主导,看栈顶元素
求解更大元素问题求解更小元素问题
顺序遍历构造单调递减栈构造单调递增栈
逆序遍历构造单调递增栈构造单调递减栈

RMQ问题

  1. 定义若出现重复元素,则管理区间归属右侧的元素。对于每个元素 v i v_i vi,预处理其 RMQ \texttt{RMQ} RMQ管理区间 ( L ′ , R ′ ) (L',R') (L,R):以区间最小值为例:

    • R ′ R' R:计算 v i v_i vi的下个 ≤ v i \le v_i vi的位置

    • L ′ L' L:计算 v i v_i vi上个 < v i <v_i <vi的位置

    v i v_i vi ( L ′ , R ′ ) (L',R') (L,R)区间内的最小值。

  2. 离线+单调栈思想:

    • 将询问离线:借助桶排序的思想,对区间右端点 R i R_i Ri维护长度为 1 1 1的桶,左端点 L i L_i Li加入桶内并排序,统计出所有 R i R_i Ri相同的区间。桶排序后,所有右端点 R i R_i Ri具有单调递增性,每个桶内的 L i L_i Li也具有单调递增性。
    • 枚举每个桶:固定右端点 R R R,序列前 R R R个数构成潜在最值集合。桶内 L i L_i Li呈单调递增性,最值的变化也呈单调性。因此借助单调栈的思想,维护一个前 R R R个元素的单调子序列。对于 max \texttt{max} max值,构造单调递减子序列;对于 min \texttt{min} min值,构造单调递增子序列。首个下标 ≥ L i \ge L_i Li的元素即为答案。
      算法流程:
      定义单调存储的是下标:
    1. 对于所有询问 [ L 1 , R 1 ] , ⋯   , [ L n , R n ] [L_1,R_1],\cdots,[L_n,R_n] [L1,R1],,[Ln,Rn],对所有右端点 R i R_i Ri维护长度为 1 1 1的桶,进行排序。排序后,所有右端点 R i R_i Ri具有单调递增性,每个桶内的 L i L_i Li也具有单调递增性;
    2. 对于询问 [ L i , R i ] [L_i,R_i] [Li,Ri],遍历序列到 R i R_i Ri,维护前 R i R_i Ri个元素的单调子序列,作为潜在最值元素集合;
    3. 二分查找单调子序列中首个 ≥ L i \ge L_i Li的下标,即为区间 [ L i , R i ] [L_i,R_i] [Li,Ri]的最值。

    时间复杂度 O ( n + q log ⁡ 2 n ) O(n+q\log_2n) O(n+qlog2n)

单调队列

单调队列:基于双端队列的数据结构,队列中数据从队头到队尾具有单调性。要求遍历的序列中每个元素都必须要进入一次单调队列。由于其单调性,不要求每个元素在遍历序列之后仍然保留在单调队列内。

单调队列的性质:

  1. 队头始终为管理区间内的最值。

  2. 由于需要对队列进行队尾入队,队头、队尾出队,故采用双端队列(deque)实现。

在这里插入图片描述
单调队列最常用于优化滑动窗口。

应用:滑动窗口区间RMQ(最值)问题、滑动窗口最大子序和问题…

滑动窗口区间RMQ问题

滑动窗口利用双指针的思想,是尺取法的应用。滑动窗口必须保证每个元素都能入队,由于滑动窗口的滑动特性,不要求每个元素都持续在队列中)

  • 最大值问题:维护单调递减队列;最小值问题:维护单调递增队列。其中队头一定是最值

本类问题中,单调队列只能从队尾入队,从队头、队尾出队

关键操作:

  1. 删头:若队头元素脱离窗口(超过窗口范围),队头元素从队头出队
  2. 去尾:若新元素在从队尾入队时,原队尾破坏了队列的单调性,使原队尾出队
int k;//窗口长度
vector<int>a;//下标从1开始
deque<int>dq;//以单增队列为例,存储为下标
void solve(){
    for(int i=1;i<=a.size();i++){
        while(dq.size()&&a[dq.back()]>=a[i]) dq.pop_back();//单减队列为<=
        dq.push_back(i);
        if(i>=k){//窗口大小已取足
            while(dq.size()&&i-dq.front()>=k) dq.pop_front();//不可写作m.size()>=k(单调队列元素下标可能非连续)
            cout<<a[m.front()]<<' ';
        }
    }
}

滑动窗口最大子序和问题

posted @   椰萝Yerosius  阅读(2)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示