[总结]单调队列

(本文不涉及DP,以后可能会更新)

单调队列是一种基于deque,通过特定的维护方式使得结构内部具有单调性的算法.

区别于单调栈,单调队列需要对队列的两端同时进行维护.

我这样描述可能会使用单调队列算法的问题:该问题需要求出一个全局的/若干个局部的解,这个(些)解与一个区间/区间的两端相关.

我发现有不少单调队列问题都有其它RMQ算法解法(https://blog.csdn.net/free__radical/article/details/40627503).

这时候,单调队列是O(n),ST表是O(nlogn).但单调队列不支持在线查询(应该是这样叫),如果Query够多就只能用ST表了.

以模板题P1886 滑动窗口 /【模板】单调队列为例,该问题要求出每个局部窗口内的区间的最值.求解最大值与最小值采用了类似方法,故只以求最小值为例.

(实际上区间最值问题应该立刻想到ST表,这题RMQ算法确实可以通过)

如果一个区间的左端点固定,右端点不断增长(右移),那么可以很容易地求解原区间在每次增长后的新的最小值.

在滑窗问题中,左端点和右端点是一起右移的,因为左端点右移可能导致原先的最小值离开区间,上述算法不可行.

故考虑构造一种数据结构,使其内部有序地存储当前区间内的最小值元素和可能成为之后区间的最小值的元素.每当左右端点右移时,删除离开了区间的元素,并且根据新进入的元素调整(包括删除操作)原有元素使其与新元素保持有序性.

考虑数据:

8 3
1 3 -1 -3 5 3 6 7

从右端点为第一个位置开始维护一个结构(方括号表示区间):

[1] 3 -1 -3 5 3 6 7

将1存入,还没有形成完整的窗口,没有需要进行的操作.现有:1

[1 3] -1 -3 5 3 6 7

将3存入,现有:1 3

[1 3 -1] -3 5 3 6 7

将-1存入,现有1 3 -1

第一个区间的最小值显然是-1.

现在在此基础上考虑之后的情况.当窗口继续滑动,只要-1还在区间内,1,3就不可能成为区间最小值.而-1只有在1,3都离开区间后才会离开区间,这意味着1,3永远不可能成为区间最小值了.

对于已经不会产生作用的数据,选择舍弃,故现有:-1

此外,不会产生作用的数据有且仅有另一种,即随着区间右移离开了区间的元素,将他们也直接删除.

推理一下发现,如果新元素的加入保持了序列原有的单调递增性,那么可以直接将新元素加入到现有序列右侧,因为左边的元素总会离开区间,此时序列内所有元素都可能成为之后的区间的最小值.

因此,按照如下方法维护一个deque:

  • 若新增元素不破坏队列内原有元素的单调性或者队列为空,使该元素从队尾入队
  • 若新增元素使得队首元素失效(除了因为破坏单调性导致的失效),使所有失效元素从队首出队
  • 若新增元素使得队尾附近的元素单调性被破坏,让队尾元素不断出队直到某一时刻,该时刻队列为空或者使新元素入队不会破坏单调性

在滑窗问题中,第二条的失效即元素从区间左侧离开,deque的单调性是从队首到队尾单调递增的.

因为是求解局部问题,窗口每次滑动都要输出一次当前解.

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <deque>
#include <iostream>
using namespace std;

typedef pair<int, int> P;
P p[1000010];
deque<P> q;
int n, k;

int main() {
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++) {
        int x;
        scanf("%d", &x);
        p[i] = make_pair(x, i);
    }

    for (int i = 1; i <= n; i++) {
        while (!q.empty() && i - q.front().second + 1 > k) q.pop_front();
        while (!q.empty() && q.back().first >= p[i].first) q.pop_back();
        q.push_back(p[i]);

        if (i >= k) printf("%d ", q.front().first);
    }
    q.clear();
    puts("");
    for (int i = 1; i <= n; i++) {
        while (!q.empty() && i - q.front().second + 1 > k) q.pop_front();
        while (!q.empty() && q.back().first <=p[i].first) q.pop_back();
        q.push_back(p[i]);

        if (i >= k) printf("%d ", q.front().first);
    }

    return 0;
}
P1886

 

P1714 切蛋糕是一个求全局最优解的问题.

这个问题即,在长度为N的区间内选区长度为M的一个子区间,求该子区间的和的最大值.

涉及到区间和的问题,故转化为前缀和,问题变为求两个点i,j(i<j),使得两点的前缀和之差s[j]-s[i]取最大值.

转化为求出若干个局部最优解中的最优解,局部问题就是对于每个点(前缀和为a),求出其左侧处在区间长度内的前缀和最小的点(前缀和为b),得局部最优解a-b.

这又是一个区间最值问题,与滑窗问题中的类似处理即可.

 

P1638 逛画展是一个求全局最优解的问题,和上一个问题一样,每求出一个局部最优解就更新一下全局最优解.

子问题是,对于一个右端点,求出一个左端点,使得在保证所构成的区间内含有所有人的画的情况下区间长度最小.由于不能保证右端点左侧总是有所有人的画,该问题可能无解.

但是这个问题很难看出来单调性,因为前两个问题都是区间最值问题,只与区间的"最"者有关,其它非最者且不可能成为最者的元素直接舍弃不会产生影响,而这里的区间不存在最者,解由区间内的元素共同贡献产生,因此舍弃元素的条件更为苛刻.

我有如下理解:相比于前两个问题中把队列中的元素视为候选左端点,此问题队列中的元素为已选元素,队首为左端点,新元素为右端点

添加新元素时不会从队尾删除元素.也就是说不会破坏"单调性".

队首元素失效当且仅当新元素与队首元素相同,此时不断删除队首元素.

所谓"单调性",可以视为长度最短,涵盖最广,也就是解的最优性.

此外,这题检查区间是否涵盖所有人的方法很值得借鉴,在这道题中也可以用到.

原文地址:https://www.cnblogs.com/Gaomez/p/14292018.html