单调队列

滑动窗口

/**
 * 暴力做法
 */
#include <iostream>
#include <limits.h>

using namespace std;

const int N = 1e6 + 10;

int n, k;
int a[N];
int vec_max[N], vec_min[N], tt_max, tt_min;

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; ++ i) cin >> a[i];
    for (int i = 0; i <= n - k; ++ i)
    {
        int maxx = INT_MIN, minn = INT_MAX;
        for (int j = i; j < i + k; ++ j)
        {
            maxx = max(maxx, a[j]);
            minn = min(minn, a[j]);
        }
        vec_max[tt_max ++] = maxx;
        vec_min[tt_min ++] = minn;
    }
    for (int i = 0; i < tt_min; ++ i) cout << vec_min[i] << ' ';
    cout << endl;
    for (int i = 0; i < tt_max; ++ i) cout << vec_max[i] << ' ';
    return 0;
}

/**
 * 正解
 * 步骤是先考虑暴力的做法,然后找到没有用的元素,比如在单调栈”距离最近的较小值“中无用的元素就是相较于当前值前面的较大值,因为它们一定更不会作为后面数据的答案,所以去掉即可
 * 本题的滑动的窗口的实现形式和队列非常相似,窗口的每次滑动都是队头弹出一个元素,队尾添加一个元素
 * 考虑在找最小值时,1 3 -1 -3 4,当3 -1 -3处于同一个窗口时,3和-1对于后面新添加的数据来说一定不会是选择的最小值,因为有-3的存在,选定的最小值就一定不会是它们,所以当-3出现后,对于后面的元素,完全没有必要再去考虑3和-1,他们也就是无用元素
 * 考虑最大值时,-3 -1 3当3出现后,对于后面的数据,因为3的存在,-3和-1显然都不会是答案,所以它们也就成为了无用元素
 * 这道题对我来说的难点在于很难想到分别去求最小值和最大值,因为我想到的就是同时求,如果没有想到分别求,那就很难想到这样的优化了,说起来不算困难,但是代码实现初步感觉有点混乱,不知道怎么去实现
 *
 * 以求最小值为例,每个数据都会进入队列,在数据m进入后,队列中已有元素中>=m的元素就一定不会是答案了,所以要先把这些数从队列中去掉,这也就完成了上面说到的去除重复元素的任务,之后再把m放进队列即可
 * 大体的思路是这样的,但是很多细节还需要注意
 * 队列要存放下标而非数值,原因是队头一定是窗口内最小元素(因为每次我们都把更大的元素弹出去了),如果能取到这个元素的前提是这个元素还在窗口内部,也就是我们需要能够定位元素位置,如果队列存储数组我们是无法定位的,所以存储下标
 *
 * 程序写完之后,感觉到正解就是用单调队列把暴力中找最小值的过程从o(N)降到了o(1)
 * 队列维护的在原数组的窗口内要么是当前的最小值,要么可能是之后的最小值的数,原数组窗口中那些保证不会是答案的数全部都被删除了
 * 而且不好理解的就是窗口向后移动时,离开窗口的数据未必是队列中的,它可能属于那些保证不会是答案的数
 */
 #include <iostream>
 
 using namespace std;
 
 const int N = 1e6 + 10;
 
 int n, k, a[N];
 int hh, tt, q[N];
 
 int main()
 {
     cin >> n >> k;
     for (int i = 0; i < n; ++ i) cin >>a[i];
     
     // 求窗口内最小值
     hh = 0, tt = -1; // 数组模拟队列时提到的初始化方式,不一定非要这么写,把细节处理好即可
     for (int i = 0; i < n; ++ i) 
     {
         // 窗口向后滑动一位,有原有数组中新的数据进入窗口
         while (hh <= tt && a[q[tt]] >= a[i]) -- tt; // 把前面更大的数据弹出队列,它们在后续的滑动过程中一定不会作为答案,hh <= tt的必要性在于需要保证q[tt]下标的合法性
         q[++ tt] = i;
         
         // 窗口向后滑动一位,有原有数组中一些数据要离开窗口
         // 此时有两种情况,离开窗口的是我们已经排除的不可能是答案的数据,也可能是当前的最小值,只有后者才是在队列中的,所以我们要做的就是判断队头元素是不是要离开窗口了,此时队列存储下标再一次派上了用场
         if (hh <= tt && q[hh] < i - k + 1) ++ hh; // a1 a2 a3 a4,如果k是3,当i到3(a4)时,那么窗口的左端点下标应该为1(a2),即3 - 3 + 1(i - k + 1),删除头节点的前提是队列不为空,即 hh <= tt
         
         if (i >= k - 1) cout << a[q[hh]] << ' '; // 从满足长度为3的节点开始输出答案
     }

    cout << endl;

    // 求窗口内最大值, 和最小值对应一下即可
     hh = 0, tt = -1;
     for (int i = 0; i < n; ++ i) 
     {
         while (hh <= tt && a[q[tt]] <= a[i]) -- tt;
         q[++ tt] = i;
         
         if (hh <= tt && q[hh] < i - k + 1) ++ hh;

         if (i >= k - 1) cout << a[q[hh]] << ' ';
     }

     return 0;
 }
posted @ 2021-01-19 19:59  0x7F  阅读(51)  评论(0编辑  收藏  举报