滑动窗口详解

1. 滑动窗口基本概念

滑动窗口是一种遍历连续区间的技术。其思想是用两个指针(通常称为左指针 L 和右指针 R)来表示一个区间,这个区间就是“窗口”。随着右指针不断右移,我们逐步加入新的元素;当区间不满足条件时,再移动左指针以缩小窗口。

例如,若我们需要求数组中长度为 k 的每个子区间的最大值,就可以考虑维护一个大小固定为 k 的窗口,不断从左侧移除旧元素、从右侧加入新元素。

此算法时间复杂度仅为O(n)。

2. 单调队列的基本思想

单调队列是一种特殊的双端队列,它保证队列中的元素是单调有序的(常见的是单调递减或单调递增)。在滑动窗口最大值问题中,我们使用单调递减队列:

  • 队列的头部始终保存当前窗口中的最大/最小元素的下标;

  • 当一个新元素进入窗口时,我们会将队列中所有比新元素小/大的元素(下标对应的值)从队列尾部移除(注意是队尾,队头是维护最大/最小值的),因为它们在新窗口中不可能成为最大/最小值;

  • 同时,我们还要保证队列中的元素都是当前窗口内的。若队列头部的下标不在窗口内(即小于窗口左边界,已经不在窗口的范围之内),就要弹出。

这样,每当窗口形成(即窗口大小达到 k),队列头部的元素就是该窗口的最大/最小值。


3. 示例讲解

考虑如下数组和窗口大小,要求求出最大值:

  • 数组:[1, 3, -1, -3, 5, 3, 6, 7]

  • 窗口大小:k = 3

初始化

  • 创建一个空的双端队列 dq 用来保存元素的下标。

  • 初始化结果列表 a 为空。

逐步处理数组中的每个元素

我们将依次处理数组中的每个元素,并维护队列 dq 的单调递减性质。

第 0 步:i = 0,元素 = 1
  • 当前队列为空,无需删除过期元素。

  • 加入新元素:队列为空,直接将下标 0 加入。

  • 队列状态dq = [0](存储的是下标,对应值为 [1])

  • 还没有形成窗口(窗口大小未达到 3),因此不记录结果。

第 1 步:i = 1,元素 = 3
  • 检查队列头部是否过期:dq[0] = 0,此时窗口还未满,无需删除。

  • 加入新元素:当前新元素 3 大于队列尾部元素 1(下标 0对应的值),因此:

    • 弹出队列尾部:移除下标 0

  • 将新元素下标 1 加入队列。

  • 队列状态dq = [1](对应值为 [3])

  • 窗口还未完全形成(i=1,小于 k-1=2),不记录结果。

第 2 步:i = 2,元素 = -1
  • 检查队列头部是否过期:队列中唯一下标 1在窗口区间 [0,2] 内,无需删除。

  • 加入新元素:当前新元素 -1 小于队列尾部对应的值 3,因此直接将下标 2 加入队列尾部。

  • 队列状态dq = [1, 2](对应值为 [3, -1])

  • 此时窗口 [i-k+1, i] = [0,2] 已经形成,队列头部下标 1 对应值 3 为最大值。

  • 将 3 加入结果列表:a = [3]

第 3 步:i = 3,元素 = -3
  • 检查队列头部是否过期:当前窗口区间 [1,3],队列头部下标 1仍在窗口内。

  • 加入新元素:新元素 -3 小于队列尾部对应的值 -1(下标 2),直接将下标 3 加入队列尾部。

  • 队列状态dq = [1, 2, 3](对应值为 [3, -1, -3])

  • 形成窗口 [1,3],队列头部下标 1 对应值 3 为最大值。

  • 将 3 加入结果列表:a = [3, 3]

第 4 步:i = 4,元素 = 5
  • 检查队列头部是否过期:当前窗口区间 [2,4],队列头部下标 1 对应的索引 1 已经不在区间内(1 < 2),因此弹出队列头部。

  • 队列状态dq = [2, 3](对应值为 [-1, -3])

  • 加入新元素:新元素 5 大于队列尾部的值:

    • 比较队尾下标 3(对应 -3):5 > -3,弹出下标 3;

    • 继续比较新的队尾下标 2(对应 -1):5 > -1,弹出下标 2;

  • 此时队列为空,将新元素下标 4 加入队列。

  • 队列状态dq = [4](对应值为 [5])

  • 形成窗口 [2,4],队列头部下标 4 对应值 5 为最大值。

  • 将 5 加入结果列表:a = [3, 3, 5]

第 5 步:i = 5,元素 = 3
  • 检查队列头部是否过期:当前窗口区间 [3,5],队列头部下标 4在区间内。

  • 加入新元素:新元素 3 小于队列尾部对应值 5(下标 4),直接将下标 5 加入队列尾部。

  • 队列状态dq = [4, 5](对应值为 [5, 3])

  • 形成窗口 [3,5],队列头部下标 4 对应值 5 为最大值。

  • 将 5 加入结果列表:a = [3, 3, 5, 5]

第 6 步:i = 6,元素 = 6
  • 检查队列头部是否过期:当前窗口区间 [4,6],队列头部下标 4在区间内。

  • 加入新元素:新元素 6 大于队列尾部对应的值 3(下标 5),因此先弹出队尾下标 5;

    • 接着,新元素 6 同样大于当前队尾下标 4对应的值 5,因此弹出下标 4;

  • 队列变为空,将新元素下标 6 加入队列。

  • 队列状态dq = [6](对应值为 [6])

  • 形成窗口 [4,6],队列头部下标 6 对应值 6 为最大值。

  • 将 6 加入结果列表:a = [3, 3, 5, 5, 6]

第 7 步:i = 7,元素 = 7
  • 检查队列头部是否过期:当前窗口区间 [5,7],队列头部下标 6在区间内。

  • 加入新元素:新元素 7 大于队列尾部对应的值 6(下标 6),因此弹出下标 6。

  • 队列为空,将新元素下标 7 加入队列。

  • 队列状态dq = [7](对应值为 [7])

  • 形成窗口 [5,7],队列头部下标 7 对应值 7 为最大值。

  • 将 7 加入结果列表:a = [3, 3, 5, 5, 6, 7]

我们以这题为例:

滑动窗口 /【模板】单调队列

题目描述

有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

输入格式

输入一共有两行,第一行有两个正整数 n,k。 第二行 n 个整数,表示序列 a

输出格式

输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值

输入输出样例

输入 #1

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

输出 #1

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

说明/提示

【数据范围】
对于 50% 的数据,1≤n≤10^5
对于 100% 的数据,1≤k≤n≤10^6,ai​∈[-2^{31},2^{31})

可以发现这题如果用ST表做空间比较容易炸,而且 n log n的时间复杂度也比较危险。

所以最好用单调队列做这题。

我们可以用a数组存数据,q队列来存下标,再使用单调队列。

首先,把已经过时的队头元素去掉:

while(!q.empty()&&q.front()<=i-m) q.pop_front();

然后,把队尾比准备进队的元素要小的出队。

while(!q.empty()&&a[q.back()]>a[i]) q.pop_back();

然后进队:

q.push_back(i);

长度超过m(可以构成一个滑动窗口)就输出:

if(i>=m){
    printf("%d ",a[q.front()]);
}

最后,模版就这么出来了:

#include
using namespace std;
dequeq;
int n,m;
int a[1000005];
int main(){
   cin>>n>>m;
   for(int i=1;i<=n;i++) cin>>a[i];
   for(int i=1;i<=n;i++){ 
        while(!q.empty()&&q.front()<=i-m)q.pop_front();//去头
        while(!q.empty()&&a[q.back()]>a[i]) q.pop_back();//去尾
        q.push_back(i);
        if(i>=m){
            printf("%d ",a[q.front()]);
        }
   }
   printf("\n");
   while(!q.empty()) q.pop_front();
   	for(int i=1;i<=n;i++){
            while(!q.empty()&&a[q.back()]=m){
                while(!q.empty()&&q.front()<=i-m) q.pop_front();
                printf("%d ",a[q.front()]);
            }
   }
   printf("\n");
   return 0;
}

 

 

 

 

 

你可能感兴趣的:(算法)