单调队列 —— 滑动窗口(滚动最大值)

一道经典的单调队列题目——[洛谷P1886 滑动窗口]。(下文开始只讨论求滚动的最大值)

暴力解法是O(n^2)的,对于每一个起点,搜一遍长度为k的子序列,求得最值——复杂度不知高到哪里去了

考虑O(n)的解法:

每一次我们求解完一个区间,仅仅只是向右挪了一个单位,也就是说左边删掉了一个元素,右边加上了一个元素,变动是很小的——除非被删掉的那一个元素是上一轮的最大值,否则最大值肯定要么不变,要么是新加进来的那个。而就是因为上一轮的最大值也许(肯定有一天)会被删掉,所以让我们这种保存上一轮最大值的方法不成立了。那是不是意味着我们要保存每一轮的次大值?那如果次大值也被删掉了,那么还要保存第三大的……

这样来推理,我们要保存的是一个单调递减的队列,支持在两端删除和插入。这就是我们要介绍的一种单调线性数据结构——单调队列。在题目要我们求解左右边界同时向同一个方向移动的区间最值问题时,一般都会用到单调队列。(扩展说明:当我们推出的dp方程只与一个特定的位置x及一些常量有关时,可以用单调队列来优化)

单调队列的插入(推入)

沿着上面的思路,我们来考虑如何插入一个数。当我的窗口往右移动一个单位的时候,会有一个元素加进来——如果这个元素要比队尾的元素大,就无法保证队列单调递减了。因此我们猜想,可以不停删除队尾的元素,直到这个元素插入之后队列显得单调。

这样的做法的正确性如何证明呢?首先我们来考虑,插入时被我们踢出的那些元素是什么?是上一轮中保存下来的一些值,并且不是最大值——把他们删掉完全没有影响。

为什么这个会完全没有影响?我们考虑对于同一个区间的同一个最大值:肯定是越靠后越好。因为这样它作为最大值被利用的次数肯定更多。(窗口不断右移,如果最大值越靠左就越早出界)。所以既然被踢出的那些元素位置比我靠左,值还比我小,那留着他们干嘛用呢?它们以后永远不会被用到,完全没有利用价值。因此我们得到了单调队列插入元素的方法:不断踢出队尾元素,直到当前数字插入后让队列仍然保持单调。

单调队列的删除(踢出)

再来考虑单调队列的删除——其实删除很简单。我们对单调队列的利用仅仅只是取队头,因为队头即为最值。所以如果队头不在目前考虑的区间内,就应该把队头扔掉。直到队头在区间内为止——而此时因为队列是单调的,所以队头一定是符合区间位置的最值。

总结一下,单调队列工作的框架:

for(...){

   while(...) 踢出队尾的影响单调性的

  推入队尾

  while(...) 踢出队头的不合法的

  得到队头作为当前答案

}

其中,元素的推入常常单独写一个push函数。

代码实现:

inline void Push(int x){
    while(h <= t && a[x] > a[q[t]]) --t;
    q[++t] = x;
}
for(int i = k; i <= n; ++i){
    Push(i);
    if(i > k){
        while(h <= t && q[h] < i-k+1) ++h;
    }
    ans += a[q[h]];
}

猜你喜欢

转载自www.cnblogs.com/qixingzhi/p/9278342.html