【题解】滑动窗口

为了解决滑动窗口,我们引入单调队列的概念。

分析题目的要求,我们需要建立一种数据结构,可以满足以下要求:

  • 可以快速读取一个区间的最大值和最小值

  • 能根据编号的大小将元素快速弹出

先分析最大值。对于上述要求,我们可以用一个单调队列来解决这个问题。

我们不妨先看一组测试数据。

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

滑动窗口的运动轨迹如下:

1] 3 -1 -3 5 3 6 7

1 3 ] -1 -3 5 3 6 7

[1 3 -1] -3 5 3 6 7 此时滑动窗口已经完全进入了数列

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

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

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

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

1 3 -1 -3 5 [3 6 7] 滑动窗口已经滑到了最右边

我们可以用一个单调递减队列来解决这个问题——我们可以在队首取到最大值。

我们用一个变量 \(i\) 来模拟窗口的右侧。任意一个时刻内,宽度为\(m\)的窗口,可以表示成一个运动的区间\([i-m+1,i]\)。我们让\(i\)从1到n循环枚举,每一次,我们都对扫描到的元素进行判断,看其能否进入队列。注意,我们使用的是一个单调队列,队列里面的元素是单调递减的,这样我们就可以在对头取到最大值。如果当前元素比队尾的元素还要大,根据单调队列的定义,若把当前元素加入到队列中,那么原来队尾的元素就会处于一个低谷状态:它是不可能成为最大值的。原因很简单:队列里面的所有元素都会往队首跑,这个“低谷状态”的队列元素最终会到达队首,而它的前一号元素会比它大。这不符合我们“在队首取得最大值”的要求。这个“低谷元素”就没有存在的必要了。

    while(head<=tail && q[tail]<=a[i])
                    --tail;
                q[++tail]=a[i];
                p[tail]=i;//p表示队列对应元素的编号

我们发现,这里单调队列的使用有一点点像“栈”。如果仅仅只是像这样子扫描,然后入队,我们还不如建立一个单调栈呢?

其实不然。单调队列有一个特点,就是既可以从队首出队,又可以从队尾出队。我们除了考虑快速取得最值,还要考虑一点:由于是滑动窗口,有些窗口内的元素最终会运动到窗口外。所以,我们还要考虑队列里面的元素是否“过时”。

由于我们是按照时间顺序将元素存入队列内,因此过时的元素更有可能出现在队首,因为队尾都是新鲜的元素。由于窗口的右边界是\(i\),我们只要判断队列元素的编号和窗口左边界的关系\(i-m+1\)就可以了。如果当前元素的编号\(rank<i-m+1\),即\(rank<=i-m\),我们就把它从队首弹出。

while(p[head]<=i-m)
                    ++head;

综上所述,我们可以用单调队列解决这个问题。分析最小值同理。

#include<bits/stdc++.h>
#define For(i,a,b) for(register int i=a;i<=b;i++)
using namespace std;

struct Mq{
    static const int nmax=1000001;
    int n,k,a[nmax];
    int q[nmax],head,tail,p[nmax];

    void read()
        {
            scanf("%d %d",&n,&k);
            for(register int i=1;i<=n;++i)
                scanf("%d",&a[i]);
        }
    void Mmax()
        {
            head=1;
            tail=0;
            for(register int i=1;i<=n;++i)
            {
                while(head<=tail && q[tail]<=a[i])
                    --tail;
                q[++tail]=a[i];
                p[tail]=i;
                while(p[head]<=i-k)
                    ++head;
                if(i>=k)printf("%d ",q[head]);
            }
            printf("\n");
        }
    void Mmin()
        {
            head=1,tail=0;
            for(register int i=1;i<=n;++i)
            {
                while(head<=tail && q[tail]>=a[i])
                    --tail;
                q[++tail]=a[i];
                p[tail]=i;
                while(p[head]<=i-k)
                    ++head;
                if(i>=k)
                    printf("%d ",q[head]);
            }
            printf("\n");
        }
}monotone_queue;

int main()
{
    monotone_queue.read();
    monotone_queue.Mmin();
    monotone_queue.Mmax();
    return 0;
}

总结一下单调队列的三部曲:

  • 判单调
  • 判过期
  • 更答案

注意一下,这三个步骤的具体顺序还是要看题目的要求。注意在扫描的过程中,只有当当前新决策的收益或代价可以确定时,才能判定单调,且必须将其插入队列。
建议根据上面的要求,在以下两种顺序中选一个:

判单调\(\rightarrow\)判过期\(\rightarrow\)更答案

判过期\(\rightarrow\)更答案\(\rightarrow\)判单调

posted @ 2018-12-20 20:58  LinearODE  阅读(1856)  评论(0编辑  收藏  举报