單調佇列與單調棧
阿新 • • 發佈:2020-07-14
單調佇列與單調棧
單調佇列
- 經典的滑動視窗問題: 求一個長度為n的序列A中所有長度為m(m < n)的子區間的最大值。n <= 2e6。
線段樹等等容易tle,我們需要一個o(n)的演算法來解決這個問題。
思路:
-
考慮每個視窗,其中必然至少有一個位置為最大值。
-
如果有一個,在這個視窗中那麼這個最大值前方的所有數字對我們都是無益的(不僅小,還退出的早);同樣如果有一個以上的最大值,那麼我們選取最後一個最大值即可(前方的最大值們不僅可以被替代,還退出的早)。
-
而對其後方的所有數字,考慮到以後在這個最大值退出視窗之後,它們擁有成為最大值的“潛力”,所以是需要考慮的。
-
但同樣,在這後方的所有數字中,如果存在 \(A_i <= A_j\)
-
所以在每個視窗中,我們只需要記錄當前的最大值以及這個最大值後面的數,當且僅當這個數的後面沒有大於等於它的數。
可以看出,這個視窗是可以用一個雙向佇列來模擬的,每當後方加入一個數,都要從佇列尾部開始淘汰掉所有的小於它的數,保證佇列中每一個數的後面,在視窗範圍內沒有大於等於它的數。當隊首元素不在視窗範圍時,隊首元素出隊。這樣操作後,隊內的元素都是單調的,所以叫作單調佇列
參考題目:洛谷P1440 (稍有不同)
P1440陣列模擬佇列程式碼:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; int n, m; struct ab { int l; //position int v; } que[2000005]; int head = 1, tail = 1; int main() { scanf("%d%d", &n, &m); printf("0\n"); for (int i = 1; i < n; ++i) { int num; scanf("%d", &num); while (tail != head && num <= que[tail - 1].v) { --tail; } que[tail].l = i; que[tail++].v = num; while (que[head].l <= i - m) { ++head; } printf("%d\n", que[head].v); } scanf("%d", &n); //吞掉最後一個數的輸入 return 0; }
單調棧
瞭解單調佇列後,單調棧讓人顧名思義想到的就是棧內所有元素是單調的
解決問題:
-
1.求一個序列中任意一個數的後方/前方的第一個比自己大/小的數的下標
-
2.求一個序列中任意一個數的後方/前方連著有多少個比自己小的數
這兩個問題本質是一樣的,但是樸素演算法顯然是 \(O(n^2)\) 的,我們要實現時間上的降維就要使用單調棧。
- 舉例來說,求一個序列中任意一個數的後方的第一個比自己大的數的下標,如果不存在則為0,序列長度n <= 3e6。
- 一開始我們在棧底放入一個INF
- 將序列從左到右依次入棧
- 當棧頂元素比當前數小時,棧頂元素出棧同時標記該棧頂元素的對應答案為入棧元素的下標,如此直到棧頂元素大於等於入棧元素
- 最終留在棧內的元素對應答案都為0
模板題:洛谷P5788
P5788陣列模擬棧程式碼
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n;
int top = 1;
struct ab
{
int v;
int l;
} sta[3000005];
int ans[3000005] = {0};
int main()
{
scanf("%d", &n);
int xx;
scanf("%d", &xx);
sta[top].l = 1;
sta[top++].v = xx;
for (int i = 2; i <= n; ++i)
{
scanf("%d", &xx);
while (top != 1 && sta[top - 1].v < xx)
{
ans[sta[top - 1].l] = i;
--top;
}
sta[top].l = i;
sta[top++].v = xx;
}
while (top != 1)
{
ans[sta[top - 1].l] = 0;
--top;
}
for (int i = 1; i < n; ++i)
{
printf("%d ", ans[i]);
}
printf("%d", ans[n]);
return 0;
}