• 浅谈单调队列:死海不是海,单调队列不是队列


    滑动窗口最值问题

    给定一个长度为n的序列a1,a2,…ai,…,an,将一个长为k的滑动窗口自序列最左端向右边滑动。例如:初始时,窗口内的子序列为a1,a2,…,ak;当窗口向右滑动一位,此时窗口内的子序列变为a2,a3,…,ak+1。

    我们要解决的问题是,给定长度为n的序列以及滑动窗口的大小k,求每一个滑动窗口内的最小值和最大值。

    以长度为5的序列1, 3, 4, 5, 7滑动窗口k=3为例说明: 

    第1个滑动窗口(1, 3, 4)的最小值、最大值分别为1和4;

    第2个滑动窗口(3, 4, 5)的最小值、最大值分别为3和5;

    第3个滑动窗口(4, 5, 7)的最小值、最大值分别为4和7。

    一些可行的解决思路

    最直接的思路,可以枚举所有窗口(一共n-k+1个),扫描窗口内的每一个元素求其最值。
    整个算法的时间复杂度是O(n*k),当数据规模较大的时候(如n=10^6,k=10^4),该算法耗时较长。

    另一个容易想到的思路,将序列建线段树(或者树状数组等等),该过程的时间复杂度是O(n*logn),再通过n-k+1次查询区间最值求每个窗口对应区间的最大(小)值。整体时间复杂度是O(n*logn)。

    单调队列:更优美的思路

    一种更加优美的解决方法是单调队列。那么,我们就来一起揭开「单调队列」的神秘面纱吧。

    首先,第一个问题来了:单调队列是队列——吗?
    字面上去理解的话,单调队列肯定是队列,没毛病。不然为啥不叫单调栈呢。
    但是,初中地理老师有言在先:死海不是海,是湖泊,还是世界上最低的湖泊。为毛不起个「死湖」的名字?!这个……就自行google/baidu吧。
    扯远了,扯回来。
    单调队列,从严格意义上讲还真不是「队列」
    什么是队列呢?就是一中FIFO(First In First Out)的数据结构。所有要入队的元素,统一从队尾入队,再从队首出队。
    但是,「单调队列」却不是一种FIFO的数据结构。在单调队列中,为了维护队列内元素的「单调」性,所有要入队的元素,统一从队尾入队,再从对首出队,也可以从对尾直接出队

    单调队列的基本操作

    听起来有点玄乎,先来看看单调队列(以递增队列为例)有哪些基本的操作。

    1.入队(push_back):对于待入队的元素,为维护队列的递增性,如果队尾元素值大于待入队元素,则将对尾元素从队列中弹出,重复此操作,直到队列为空或者队尾元素小于待入队元素。然后,再把待入队元素添加到队列末尾。

    2.出队(pop):分被动的出队(为维护队列单调性,将元素从队尾弹出)和主动的出队(和传统的队列一样,从队首出;但是有讲究,正是这个讲究让滑动窗口最值问题得以解决)。

    利用单调队列求解滑动窗口最值问题

    下面,一起来看看如何利用单调(递增)队列来解决滑动窗口的最(小)值问题。

    以长度为6的序列1, 3, 4, 5, 7, 2和滑动窗口k=3为例:

    1)1入队,入队后队列变为[1];

    2)3入队,3大于队尾元素1,入队后队列变为[1, 3];

    3)4入队,4大于队尾元素3,入队后队列变为[1, 3, 4];

    从4开始,已经形成了第1个滑动窗口,窗口内最小值就是队首元素1。

    4)5入队,5大于队尾元素4,入队后队列变为[1, 3, 4, 5];

    这时,队内有4个元素,求第2个滑动窗口内最小值的策略是:

    取出队首元素,如果该元素不在滑动窗口内,则将其从队列中弹出,继续取新的对首元素,直到队首元素出现在窗口内;此时,队首元素即为窗口最小值。

    这也就是出队操作的「讲究」之处。

    在求得第2个滑动窗口的最小值后,1由于不在滑动窗口内被弹出,队列变为[3, 4, 5];

    5)7入队,7大于队尾元素5,入队后队列变为[3, 4, 5, 7];

    求得第3个滑动窗口的最小值,3由于不在窗口内出队,4在窗口内,所以4为第3个窗口的最小值。队列变为[4, 5, 7]。

    6)2入队,为维护队列的单调性,依次弹出7, 5, 4,完成入队后,队列变为[2]。

    求得第4个滑动窗口最小值为2,队列保持不变,依然为[2]。

    时间复杂度

    理解单调队列的核心之一在于,所有被动的出队(在队尾被弹出)的元素,都不可能是当前所求窗口的最值。

    由于序列中的每个元素只可能入队1次,最多也可能出队1次,所以均摊下来,用单调队列求滑动窗口内最小值的算法时间复杂度是O(n)。

    类似地,也可以利用单调递减队列来求得滑动窗口内的最大值问题。

    单调队列的一个更加实用的用途,就是利用其滑动窗口最值优化动态规划问题的时间复杂度。

    另外,关于这个问题,你可以在这里小试牛刀。

    c++源码实现

     1 #include <iostream>
     2 #include <vector>
     3 #include <deque>
     4 #include <cstdio>
     5 
     6 #define MAXN 10010
     7 
     8 class Data {
     9 public:
    10     int val;
    11     int idx;
    12     Data() { val = idx =  0; }
    13     Data(int x, int y):val(x), idx(y){}
    14 };
    15 
    16 class OrderedQueue {
    17 private:
    18     Data que[MAXN]; //在部分机器上(如POJ的环境上,MAXN为10^6时,会出现Runtime Error,一种可行的方法是将其设置为全局变量(由于封装差,因此不提供这个版本的代码).
    19     int front;
    20     int back;
    21     int window_size;
    22     // true -> increasing(not strictly)
    23     // false -> decreasing(not strictly)
    24     bool order;
    25 public:
    26     OrderedQueue();
    27     OrderedQueue(int window_size, bool order);
    28     void push_back(Data d);
    29     Data get_window_front(int pos);
    30     void clear();
    31     bool empty();
    32 };
    33 
    34 OrderedQueue::OrderedQueue() {
    35     window_size = 3;
    36     order = true;
    37     clear();
    38 }
    39 
    40 OrderedQueue::OrderedQueue(int window_size, bool order) {
    41     this->window_size = window_size;
    42     this->order = order;
    43     clear();
    44 }
    45 
    46 void OrderedQueue::clear() {
    47     front = back = 0;
    48 }
    49 
    50 bool OrderedQueue::empty() {
    51     return front == back;
    52 }
    53 
    54 void OrderedQueue::push_back(Data d) {
    55     while (front < back) {
    56         Data tail = que[back - 1];
    57         bool tag = order ? d.val > tail.val : d.val < tail.val;
    58         if (tag) {
    59             break;
    60         } else {
    61             back--;
    62         }
    63     }
    64     que[back++] = d;
    65 }
    66 
    67 Data OrderedQueue::get_window_front(int pos) {
    68     while (front < back && que[front].idx < pos - window_size + 1) {
    69         front++;
    70     }
    71     return que[front];
    72 }
    73 
    74 
    75 int main() {
    76     int a[8] = {1, 3, -1, -3, 5, 3, 6, 7};
    77     int wsize = 3;
    78     OrderedQueue oq1 = OrderedQueue(wsize, true);
    79     OrderedQueue oq2 = OrderedQueue(wsize, false);
    80     for (int i = 0; i < 8; i++) {
    81         oq1.push_back(Data(a[i], i));
    82         oq2.push_back(Data(a[i], i));
    83         if (i + 1 >= wsize) {
    84             std::cout << "在区间[" << (i - wsize + 1) << "," << i << "]内的最小值为" <<  oq1.get_window_front(i).val << std::endl;
    85             std::cout << "在区间[" << (i - wsize + 1) << "," << i << "]内的最大值为" <<  oq2.get_window_front(i).val << std::endl;
    86         }
    87     }
    88     return 0;
    89 }

     

      

    本文为原创博文,如需转载请注明文章出处以及作者信息。

    微信扫二维码打赏,鼓励一下吧! 

     

  • 相关阅读:
    正确使用 Volatile 变量
    什么叫持久化?
    大型J2EE项目中的Web容器集群–Nginx+Glasshfish+Memcached+ServletFilter
    REST
    Java多线程设计模式:wait/notify机制
    Java Persistence API (JPA) 的陷阱
    JDK1.5新特性介绍
    用Amazon EC2搭建免费WordPress博客及SSH
    PDF Split and Merge Basic 好用的PDF合并分割工具
    路威机器人
  • 原文地址:https://www.cnblogs.com/saywhy/p/6726016.html
Copyright © 2020-2023  润新知