单调队列的学习 - 滑动窗口求最大/小值

这几天在学习单调队列和单调栈,感觉下面几篇博客讲的比较好。
http://www.cnblogs.com/saywhy/p/6726016.html
http://dping26.blog.163.com/blog/static/17956621720139289634766/

单调队列不是真正的队列。因为队列都是FIFO的,统一从队尾入列,从对首出列。但单调队列是从队尾入列,从队首队尾出列,所以单调队列不遵守FIFO。

1) 对于单调(递增)队列,每次添加新元素时跟队尾比较,如果队尾元素值大于待入队元素,则将对尾元素从队列中弹出,重复此操作,直到队列为空或者队尾元素小于待入队元素。然后,再把待入队元素添加到队列末尾。
单调递增是指从队首到队尾递增。举个例子[1,3,4,5],1是队首,5是队尾。
单调(递减)队列与之类似,只是将上面的大,小两字互换。

2)单调(递增)队列可以用来求滑动窗口的最小值。同理,单调(递减)队列可以用来求滑动窗口的最大值。其算法复杂度都是O(n)。注意,如果我们用最小堆或是最大堆来维持滑动窗口的最大/小值的话,复杂度是O(nlogn),因为堆查询操作是O(1),但是进堆和出堆都要调整堆,调整的复杂度O(logn)。

3) 单调队列的一个用途是利用其滑动窗口最值优化动态规划问题的时间复杂度。

单调队列如何实现呢? 以单调递增队列为例,我看到网上有些实现代码类似如下:

int q[maxn];
int front, tail, cur;
front = tail = 0;
while (front < tail && q[tail-1] > cur)
     --tail; 
q[tail++]=cur; 

单调递减队列类似,只是q[tail-1]元素的比较改为<即可。

但实际上单调队列光以上代码是不够的,因为单调队列的窗口是有限的,所以当tail移动时,也会带动front移动,如果它们之间的距离超过了窗口大小的话。所以我们必须记得更新front。注意,单调队列的front和tail两个指针是不断往同一个方向移动的。

怎么更新呢? 我们必须保存队列中每个元素在原数组中的下标。我们可以把这些坐标保存在q[]中(注意上面的代码里面的q[]是保存的元素的值),然后a[q[i]]就是对应元素的值了。当我们发现head和tail所指向的数组元素的gap大于sliding window的大小时,就必须调整head了。

以单调递增队列求滑动窗口最小值为例,代码如下:

#include <iostream>
using namespace std;
#define maxn 1000006
int a[maxn];
int q[maxn];

int main()
{
    int n,k,i;
    int head=0, tail=0;

    scanf("%d%d",&n,&k);  //array size and sliding window size

    for(i=0;i<n;i++)
        scanf("%d",&a[i]);

    for (i=0; i<n; i++)
    {
        while(head<tail && a[i]<=a[q[tail-1]])
            tail--;

        q[tail++]=i;

        //head和tail的间隔已超出sliding window的size, 需要更新head, 使得     
        //head和tail的间隔刚好是sliding window的size。
        if (tail> 0 && (q[tail-1]-q[head]+1>k))
           head += q[tail-1] + 1 -k -q[head];

        if (i>=k-1)
           cout<<a[q[head]]<<" ";
    }
    cout<<endl;
    return 0;
}

有几点需要注意的地方:
1) head和tail都往增大的方向移动,tail永远>=head。
2) 注意tail-1的用法,因为tail++。
3) 调整head的时候一步就够了,但是因为tail-1,所以要加一个判断条件tail>0以防止刚开始的时候head就被调整。
4) 如果我们要实现单调递减队列求sling window的最大值,则将上面的

while(head<tail && a[i]<=a[q[tail-1]])

改成

while(head<tail && a[i]>=a[q[tail-1]])

即可,其它不变。
5) 上面的算法还有优化空间。在

while(head<tail && a[i]<=a[q[tail-1]])

里面可以用binary search。因为是单调队列嘛。

下面讨论一下该算法的复杂度。上面的代码for循环里面又有while循环,看起来复杂度好像是O(nk),如果采用binary search来找也是O(nlogk)。但是我们换个角度想,每个元素最多入队一次,出队一次,所以出队入队操作一共2n个,这2n个操作平摊到n个元素中,所以复杂度实际上是O(k)。具体分析可以参考算法里面的amortized analysis。如果我们在while循环里面用binary search, 对于slidiing window很大的情况,可以加快点速度,但复杂度还是O(k)。

另外,单调队列也可以用deque实现。因为deque支持队尾和队头两边的出列和入列的操作。下面是用单调递增队列求滑动窗口最小值的代码:

#include <iostream>
#include <deque>
using namespace std;

#define maxn 1000006
int a[maxn];
deque<int> dq;


int main()
{
    int n,k,i;
    scanf("%d%d",&n,&k);  //array size and sliding window size

    for(i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
    }

    for (i=0; i<n; i++)
    {
       while(!dq.empty() && a[i]<a[dq.back()])
           dq.pop_back();

       dq.push_back(i);

       while(dq.back() - dq.front() + 1 > k)
          dq.pop_front();

       if (i>=k-1)
           cout<<a[dq.front()]<<" ";
    }

    cout<<endl;
    return 0;
}

注意:
1) 在pop_front()的操作中,必须逐一pop出front(),不能直接调整front的位置,因为会破坏deque内部的iterator。这里复杂度仍然是O(n),详见上面的amortized analysis。
2)用deque不用操心tail-1的问题,因为deque内部会处理。
3)若用单调递减队列求滑动窗口最大值,只需将下面while循环内的

      while(!dq.empty() && a[i]<a[dq.back()])

<改成>即可,其它不变。

猜你喜欢

转载自blog.csdn.net/roufoo/article/details/78443281