单调队列总结

声明:文章同步发表于我的洛谷博客

不得不说,这玩意是真的烧脑。我至少学了三遍才学会。

我们先看一道题:

P1886 滑动窗口 /【模板】单调队列

作为一名 OIer,我们肯定是优先上暴力大法(暴力该怎么写不用我说了吧)。

我们先拿单调栈试试(数据:1 3 -1 -3 5 3 6 7,区间长度为 3,先找最大值):

1 入栈,栈变为 [1]

3 入栈,栈变为 [1,3]

-1 入栈,栈变为 [-1](这还是求最大值吗?)。

-3 入栈,栈变为 [-3]

5 入栈,栈变为 [-3,5]

3 入栈,栈变为 [-3,3](这是区间吗……)。

6 入栈,此时我们现在遇到了一个问题:怎么把 -3 扔出去。

到了这里,你应该就能发现,栈由于本身容器的限制,无法方便地实现窗口最值查询(当然如果你能用单调栈做出来那必须 Orz)。

因此我们考虑使用队列进行维护,准确来说是双端队列。

STL 双端队列(deque)使用方法(局部):

成员函数功能
deque<int>q定义一个 int 类型的双端队列 q
q.size()返回双端队列中的元素数量
q.empty()判断队列是否非空,为空返回 true,否则返回 false
q.front()返回队列的队头元素
q.back()返回队列的队尾元素
q.clear()将队列清空
q.pop_front()从队头弹出一个元素
q.pop_back()从队尾弹出一个元素
q.push_front()从队头压入一个元素
q.push_back()从队尾压入一个元素

接下来我们手动模拟一个求区间最大值(区间长度为 3)的单调队列:

数据:1 3 -1 -3 5 3 6 7。

1 进队,队列变为 [1]

3 进队,1<3,队列变为 [3]

-1 进队,-1<3,队列变为 [3,-1]

-3 进队,-3<-1,队列变为 [3,-1,-3]

3 的范围越界,队列变为 [-1,-3]5 进队,队列变为 [5]

3 进队,3<5,队列变为 [5,3]

6 进队,队列变为 [6]

7 进队,6<7,队列变为 [7]

模拟结束。

如果这不够形象的话就看看题目提供的图吧:

单调队列在算法竞赛中主要有两个应用点:

1. 求解固定长度的区间最值。
2. 优化动态规划转移过程。

Q:单调队列是否适用于所有类型的数据?

A:不是的。单调队列主要能处理有这几种特点的数据:

1. 有序性。单调队列适用于具有潜在单调性趋势的数据序列。比如在滑动窗口问题中,数据是按照一定的顺序(如数组的索引顺序)排列的,并且窗口内的数据元素之间存在大小比较关系。如果数据是完全无序的,并且没有明显的顺序可以利用来建立单调性,那么单调队列就不太适用(也就是说你很难找到数据之间的比较方式)。

2. 数据规模较大。如果某道题,1\le n\le 10^4,要你求出某一段的区间最值。用暴力枚举就能做,那么用单调队列做就有点“大炮打蚊子——大材小用”的感觉了。

3. 最值查询问题。单调队列非常适合用于求解区间最值问题,如果是求和,求异或和,求最大公因数等其他方面的问题,单调队列就不太适用了(当然我没说不可以用)。

好了,回归正题,我们现在该解决的问题是:P1886 该怎么解决?

不是吧到了这里应该会写 P1886 了吧。

我们维护两个单调队列,一个用来维护区间最小值,一个用来维护区间最大值。

主要步骤如下:

1. 将已经超出区间范围的元素从队头弹出。

   示例:
 

while(q.size()&&q.front()+m<=i){
        q.pop_front();
   }


3. 从队尾开始,如果队尾元素大于/小于当前元素的值,那么从队尾弹出元素:

   示例(维护区间最小值):
 

 while(q.size()&&a[q.back()]>=a[i]){
       q.pop_back();
   }


4. 从队尾压入当前元素:

   示例:
 

q.push_back(i);

忠告:在查询/删除容器中的元素之前,一定要判断容器是否非空,否则在赛场上 RE 了别怪我没提醒你……而且建议大家在判断队头元素是否在区间范围内时,从减法判断转化为加法判断。因为有些题要开 unsigned long long 才能过,此时用减法判断就有可能出错。

实现

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,t,a[1000005],X;
deque<int>qmin,qmax;
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=n;i++){
        while(qmin.size()&&qmin.front()+m<=i){
            qmin.pop_front();
        }
        while(qmin.size()&&a[qmin.back()]>=a[i]){
            qmin.pop_back();
        }
        qmin.push_back(i);
        if(i>=m){
            cout<<a[qmin.front()]<<' ';
        }
    }
    cout<<'\n';
    for(int i=1;i<=n;i++){
        while(qmax.size()&&qmax.front()+m<=i){
            qmax.pop_front();
        }
        while(qmax.size()&&a[qmax.back()]<=a[i]){
            qmax.pop_back();
        }
        qmax.push_back(i);
        if(i>=m){
            cout<<a[qmax.front()]<<' ';
        }
    }
    return 0;
}


P8661 [蓝桥杯 2018 省 B] 日志统计

挺简单的,甚至都不需要比较……

我们先将点赞记录按照时间从小到大排序,然后遍历每个点赞记录。

如果点赞记录对应的帖子还不是“热帖”,那么我们先将对应的单调队列中的无用元素(时间超过限制)弹出,然后将新纪录压入单调队列中,如果此时单调队列中的元素大于等于 K,说明这个帖子成了“热帖”,标记答案。

实现

#include<bits/stdc++.h>
using namespace std;
int n,k,d;
deque<int>q[100005];
pair<int,int>a[100005];
bool ans[100005];
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>d>>k;
    int maxid=0;
    for(int i=1;i<=n;i++){
        cin>>a[i].first>>a[i].second;
        maxid=max(maxid,a[i].second);
    }
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++){
        if(!ans[a[i].second]){
            while(q[a[i].second].size()&&a[i].first-q[a[i].second].front()>=d){
                q[a[i].second].pop_front();
            }
            q[a[i].second].push_back(a[i].first);
            if(q[a[i].second].size()>=k){
                ans[a[i].second]=true;
            }
        }
    }
    for(int i=0;i<=maxid;i++){
        if(ans[i]){
            cout<<i<<'\n';
        }
    }
    return 0;
}

### 单调队列与单调栈的原理、区别及应用场景 #### 一、单调栈的原理 单调栈是一种特殊的栈结构,其核心特点是栈内的元素保持单调性(递增或递减)。在实际应用中,当新元素入栈时,会从栈顶开始移除所有破坏单调性的元素,然后将新元素压入栈中。这种特性使得单调栈特别适合解决“Next Greater Element”类问题,即找到每个元素右侧第一个比它大的元素[^1]。 ```python def next_greater_element(nums): stack = [] result = [-1] * len(nums) # 初始化结果数组 for i in range(len(nums)): while stack and nums[stack[-1]] < nums[i]: idx = stack.pop() result[idx] = nums[i] stack.append(i) return result ``` #### 二、单调队列的原理 单调队列是一种特殊的队列结构,其核心特点是队列内的元素保持单调性(递增或递减)。在实际应用中,当新元素入队时,会从队尾开始移除所有破坏单调性的元素,然后将新元素加入队尾。同时,当滑动窗口移动时,需要从队首移除那些已经不在当前窗口范围内的元素[^4]。这种特性使得单调队列特别适合解决滑动窗口最大值或最小值问题。 ```python from collections import deque def sliding_window_maximum(nums, k): queue = deque() result = [] for i in range(len(nums)): while queue and nums[queue[-1]] <= nums[i]: queue.pop() queue.append(i) if queue[0] <= i - k: queue.popleft() if i >= k - 1: result.append(nums[queue[0]]) return result ``` #### 三、单调栈与单调队列的区别 1. **数据结构类型**:单调栈是基于后进先出(LIFO)的数据结构,而单调队列是基于先进先出(FIFO)的数据结构。 2. **操作顺序**:单调栈的操作主要发生在栈顶,而单调队列的操作既涉及队首(移除过期元素),也涉及队尾(维护单调性)。 3. **适用场景**:单调栈适用于解决“Next Greater Element”类问题,而单调队列适用于解决滑动窗口最大值或最小值问题[^2]。 #### 四、应用场景 - **单调栈的应用场景**: - 寻找数组中每个元素右侧第一个比它大或小的元素[^1]。 - 计算直方图中最大的矩形面积[^2]。 - **单调队列的应用场景**: - 求解滑动窗口内的最大值或最小值[^3]。 - 时间序列的范围查询[^3]。 #### 五、总结 单调栈和单调队列都是高效的算法工具,能够显著降低时间复杂度。选择使用哪种结构取决于具体问题的需求。如果问题是关于元素间有序性的问题,如“Next Greater Element”,则优先考虑单调栈;如果问题是关于滑动窗口的最大值或最小值,则优先考虑单调队列。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值