滑动窗口——单调队列

滑动窗口

给定一个大小为 n≤106的数组。

有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k为 3。
窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。第一行包含两个整数 n和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n个整数,代表数组的具体数值。同行数据之间用空格隔开。

输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:

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

输出样例:

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

思路:

  1. 如果用普通队列如何做。
    队列可以模拟滑动窗口不断从前弹出元素,从后进入元素的过程。
  2. 优化:去掉一定没用的元素——队列变成单调。
    哪些元素是一定没用的呢?
    以求滑动窗口中最小值举例,找性质规律
    若一个窗口的大小为6,值分别为:5 3 1 4 2 7
    在之后挑选最小值的过程中,5和3还有没有可能被选为最小值?
    答案是没有了,因为在向后滑动的过程中,如果5和3在窗口中,那么1一定也在窗口中,而1小于5 3,所以至少选择1,而不会选择5 3。
    同理,4也没有可能被选为最小值了,因为如果4在窗口中,1 2 至少有一个也在窗口中,所以也不会选到4的。
    那2 7 有没有可能被选为最小值呢?是有可能的,因为目前不知道2 7之后的元素大小,在窗口中比2 7小的比如1,但是当2 7在窗口中时,1可能已经被弹出窗口了,所以2 7 是有可能被选为最小值的。
    进一步归纳分析
    窗口向右滑动 可以发现以下性质:当前在窗口中的元素,左边的元素会早于右边的元素弹出窗口,左边的元素存在时,右边的元素一定也存在于窗口中。
    基于上述性质:在当前窗口中,左边较大的元素一定不会被选为最小值,因为在窗口继续向右滑动的过程中,若左边较大的元素在窗口中,右边较小的元素一定也在窗口中,则轮不到左边较大的元素作为最小值。
    将每个元素的左边较大的值全部删掉后,队列中的每个值的左侧都小于等于该值,则队列为单调递增的。
    选最大值的情况反向对称即可。
  3. 队头即为最大值或最小值。

写代码时,操作的先后顺序的逻辑要理顺:
每一轮:
1. 在队中去掉弹出窗口的元素
2. 删除左侧较大的值,并找到下个进队元素的位置,并进队
3. 输出队头元素(最小值)

单调队列并不是纯粹的队列,因为需要再队的尾部去除元素,因此用数组操作比较方便。

c++代码:

#include

using namespace std;

const int N=1e6+5;
int n,k;
int a[N],q[N];

int main(){
    cin>>n>>k;
    for(int i=0;i<n;i++) cin>>a[i];
    int hh=0,tt=-1;
    for(int i=0;i<n;i++){
    //逻辑要理顺:
    /*每一轮:
    1.在队中去掉弹出窗口的元素
    2.删除左侧较大的值,并找到下个进队元素的位置,并进队
    3.输出队头元素(最小值)
	*/
        if(i>=k&&q[hh]==a[i-k]) hh++;
        while(tt>=hh&&q[tt]>a[i]) tt--;
        q[++tt]=a[i];
        if(i>=k-1) cout<<q[hh]<<" ";
        
    }
    cout<<endl;
    
    hh=0,tt=-1;
    for(int i=0;i<n;i++){
        if(i>=k&&q[hh]==a[i-k]) hh++; 
        while(tt>=hh&&q[tt]<a[i]) tt--;
        q[++tt]=a[i];
        if(i>=k-1) cout<<q[hh]<<" ";
    }

}



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