• 【题解】滑动窗口


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

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

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

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

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

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

    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("
    ");
            }
        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("
    ");
            }
    }monotone_queue;
    
    int main()
    {
        monotone_queue.read();
        monotone_queue.Mmin();
        monotone_queue.Mmax();
        return 0;
    }
    

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

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

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

    判单调( ightarrow)判过期( ightarrow)更答案

    判过期( ightarrow)更答案( ightarrow)判单调

  • 相关阅读:
    同一电脑登录多个github账号
    如何用HAProxy+Nginx实现负载均衡
    Windows10下 tensorflow-gpu 配置
    机器学习数据处理时label错位对未来数据做预测
    机器学习经典模型简单使用及归一化(标准化)影响
    学机器学习,不会数据处理怎么行?—— 二、Pandas详解
    版本控制系统 git 之基础讲解
    学机器学习,不会数据处理怎么行?—— 一、NumPy详解
    Reinforcement Learning 的那点事——强化学习(一)
    读研 or 工作?对计算机类专业学习的看法
  • 原文地址:https://www.cnblogs.com/LinearODE/p/10152005.html
Copyright © 2020-2023  润新知