滑动窗口与单调队列

引出

见下面的模板题

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

题目描述

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

例如:

The array is [ 1 , 3 , − 1 , − 3 , 5 , 3 , 6 , 7 ] [1,3,-1,-3,5,3,6,7] [1,3,1,3,5,3,6,7], and k = 3 k = 3 k=3

输入格式

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

输出格式

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

样例 #1

样例输入 #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 % 50\% 50% 的数据, 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105
对于 100 % 100\% 100% 的数据, 1 ≤ k ≤ n ≤ 1 0 6 1\le k \le n \le 10^6 1kn106 a i ∈ [ − 2 31 , 2 31 ) a_i \in [-2^{31},2^{31}) ai[231,231)

解决

滑动窗口问题需要我们求出每个窗口内的最大(最小)值,如果使用朴素的做法需要 O ( N × K ) O(N \times K) O(N×K)时间复杂度。听滑动窗口这个名称就容易联想到一个线性的算法,因为这个形象的名字让我们感觉窗口是线性滑动的。

下面以求窗口中的最大值为例进行说明
假设我们要求窗口中的最大值,我们能否使用一个变量来维护这个最大值呢?

  • 每当窗口向右滑动一个窗格,判断当前窗口最右端新加入的数是否比当前最值大,更新最值
  • 同时我们选择维护最值在原序列中对应的下标而不是其值,这样我们可以判断什么时候应该将其出队(舍弃)
  • 如果假设当前的最值(的下标)处于窗口的最左端,而窗口的最右端可以通过窗口的长度得出,若当前移动到的右端的下标大于窗口的最右端,不成立,需要将下标舍弃,也就是说当前最值(下标) M A X p o s MAX_{pos} MAXpos和当前新滑入窗口的元素的位置 i i i要满足: M A X p o s + k > i MAX_{pos}+k>i MAXpos+k>i k k k为滑动窗口的长度

这个想法的前两部是没问题的,但是第三步中假设我们要舍弃当前的最大值,那么谁应该成为最大值呢?是新滑入的元素吗?假设原来的窗口中有比新滑入的元素大而又比原来最值小的元素,应该是它们成为最大值,而这些信息在这个思路中没有被维护,所以是行不通的

我们需要考虑谁能贡献成为当前滑动窗口最大值的一部分,首先明确一个非常重要的点,这个是想到使用单调队列来解决的突破口:

在滑动窗口中如果一个数 a a a 比另一个数 b b b 小而 a a a 又出现在 b b b 的前面(即 a a a 先于 b b b 滑入窗口),那么这个小的数 a a a对滑动窗口中的最大值没有贡献应该被舍去

证明可以分两步进行,分别证明去除这个数对当前滑动窗口的最值和之后滑到的滑动窗口的最值没有影响。

那么下面引入单调队列的解法:

  • 如果当前滑入窗口的数比先滑入窗口的数大,那么就说明了当这个数滑入窗口时和滑入窗口之后,原来在队列中的元素(比它小)就不会再对后面的答案产生影响了,证明方法同上。那么就应该将这些(比它小)的元素弹出
  • 如果当前滑入窗口的数比先滑入窗口的数小,虽然当前可能不会产生影响,但是当前面滑入窗口的数因为窗口的滑动应该从队列中去除的时候,这个数可能成为最大值,应当加入队列

通过观察可以看到使用单调队列解决滑动窗口问题,我们不仅维护了一个元素单调递减的单调队列(但是这里为了方便地舍弃需要出队的元素,我们存储值的下标),并且这个队列的入队顺序和在队列中存储的时间顺序(滑入顺序)也是有序的,也就是说,不会出现后入队的元素应为值大而跑到队的尾部去,这样子在舍弃元素时候我们应直接从队尾考虑,这对于理解问题也是非常重要的,读者应该弄清楚这个入队顺序和在队中顺序的问题。

代码

// Problem: P1886 滑动窗口 /【模板】单调队列
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1886
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pi;
const int N = 2e6 + 10;
const int MOD = 1e9 + 7;
#define endl '\n'
#define PY puts("Yes")
#define PN puts("No")
int a[N];
deque<int> dqa, dqb;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 1; i <= n; i++)
    {
        if (dqb.size() && i > k && dqb.front() + k <= i)
        {
            // cout << "out of window " << a[dqb.front()] << endl;
            dqb.pop_front();
        }
        while (dqb.size() && a[i] < a[dqb.back()])
        {
            // cout << "delete from back " << a[dqa.back()] << endl;
            dqb.pop_back();
        }
        dqb.push_back(i);
        if (i >= k)
            cout << a[dqb.front()] << " ";
        // for (auto it = dqb.begin(); it != dqb.end(); ++it)
        // cout << a[*it] << " ";
        // cout << endl;
    }
    cout << endl;
    for (int i = 1; i <= n; i++)
    {
        if (dqa.size() && i > k && dqa.front() + k <= i)
        {
            // cout << "out of window " << a[dqa.front()] << endl;
            dqa.pop_front();
        }
        while (dqa.size() && a[i] > a[dqa.back()])
        {
            // cout << "delete from back " << a[dqa.back()] << endl;
            dqa.pop_back();
        }
        dqa.push_back(i);
        if (i >= k)
            cout << a[dqa.front()] << " ";
        // for (auto it = dqa.begin(); it != dqa.end(); ++it)
        // cout << a[*it] << " ";
        // cout << endl;
    }

    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值