滑動窗口最值問題
給定一個長度為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 }
本文為原創博文,如需轉載請注明文章出處以及作者信息。
微信掃二維碼打賞,鼓勵一下吧!
![]()