淺談單調隊列:死海不是海,單調隊列不是隊列


滑動窗口最值問題

給定一個長度為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 }

 

  

本文為原創博文,如需轉載請注明文章出處以及作者信息。

微信掃二維碼打賞,鼓勵一下吧! 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM