[LeetCode 239] Sliding Window Maximum (Queap/佇列快速求最大值)
239. Sliding Window Maximum
Given an array nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position.
For example,
Given nums = [1,3,-1,-3,5,3,6,7], and k = 3.
Window position Max
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
Therefore, return the max sliding window as [3,3,5,5,6,7].
題解:
題目需要快速求連續區間的最大值,為了使總體複雜度為線性,需要求最大值的複雜度為O(1),因此不能使用線段樹、樹狀陣列等方法來求。得益於我們需要求的區間每次移動只有1個單位,我們可以通過佇列來模擬這個區間的運動,而佇列通過巧妙的設計可以做到O(1)求佇列最值。(Queap)
為了給佇列增加getMax()操作,我們需要多設定一個雙端佇列deq維護最大值的資訊。在push一個新值x時,deq也在隊尾新增1個值x,如果前面的值比x小,則將其彈出,直到隊空或遇到比x大的元素,然後填充回等於彈出數量的x,最後再將x加入。因為填充的全是一樣的值x,因此可以只加入一個元素,然後給這個元素一個計數值cnt表示x的數量。(這是減少複雜度的關鍵)
當進行pop操作時,deq也需要將隊首出隊,因為deq中的元素有計數值cnt,因此當cnt大於1時,將cnt減1,否則才彈出。在push和pop中這樣維護後,可以發現deq的隊首元素就是當前佇列的最大值,而且不難驗證,push和pop的複雜度均攤後仍然是O(1)的。
得到這個佇列之後,求得需要的向量是簡單的。總體的時間複雜度為O(n)
程式碼:
#include <algorithm>
#include <queue>
struct Cnt {
int val;
int cnt;
Cnt(int val, int cnt) : val(val), cnt(cnt) {}
};
class Queap {
public:
Queap() {}
void push(int x);
int pop();
int getMax();
private:
queue<int> que;
deque<Cnt> cnt;
};
void Queap::push(int x)
{
que.push(x);
int count = 1;
while (!cnt.empty() && x > cnt.back().val) {
count += cnt.back().cnt;
cnt.pop_back();
}
cnt.push_back(Cnt(x, count));
}
int Queap::pop()
{
int ret = que.front(); que.pop();
if (cnt.front().cnt > 1) {
cnt.front().cnt -= 1;
} else cnt.pop_front();
return ret;
}
int Queap::getMax()
{
return cnt.front().val;
}
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
Queap que;
vector<int> ret;
if (nums.size() == 0) return vector<int>();
int i = 0;
for (; i < k; i++)
que.push(nums[i]);
ret.push_back(que.getMax());
for (; i < nums.size(); i++) {
que.pop();
que.push(nums[i]);
ret.push_back(que.getMax());
}
return ret;
}
};