單調棧
一、定義
單調棧:棧內的元素,按照某種方式排序下(單調遞增或者單調遞減),如果新入棧的元素破壞了單調性,就彈出棧內元素,直到滿足單調性。
二、用途
它可以很方便地求出某個數左邊或者右邊第一個比它大或者小地元素,而且總時間複雜度O(N)。
三、維護方法(以維護單調遞增棧爲例)
進棧操作:每次入棧前先檢驗棧頂-元素和進棧元素 x 的大小,如果小於 x,就讓 x 直接進棧。如果棧頂元素大於等於 x,那麼出棧,知道棧空或者棧頂元素小於 x。
例如:1 4 3 6 0
- 初始時刻棧空,1 入棧。———— 棧內元素(1)
- 4 要進棧,4 大於 1,所以直接進棧。———— 棧內元素(1,4)
- 3 要進棧,3 小於 4,4 出棧,3 進棧。———— 棧內元素(1,3)
- 6 要進棧,6 大於 3,6 直接進棧。———— 棧內元素(1,3,6)
- 0 要入棧,1,3,6 都出棧。———— 棧內元素(0)
例題:POJ 2559
題意:給出一個柱狀圖,它的每個矩形的的寬度是 1,高度和具體問題有關。現在要你求出柱狀圖中的最大面積(n<=1e5)
例如:2 1 4 5 1 3
最大面積是:8
逐個考慮每個項目,如何得到每個項目被包含的長方形呢?
假設考慮到每個項目 x,通過簡單的思考,我們可以得出結論,x 的左右兩邊的高度都不能比 x 低,那麼如何求出項目 x 的兩邊延申長度?
我們想到了單調棧,考慮最優的情況,以向左延伸爲例,
- 對於該長方形來說,如果它左邊的矩形比他高,那麼就添加到該項目中,知道旁邊的矩形高度比他小,那麼就是要找尋左邊第一個高度比他小的長方形位置。
- 向右延伸的情況同上
//左延伸
int t = 0;
for(int i=0;i<n;i++){
while(t&&a[st[t-1]]>=a[i])
t--;
if(t) lmin[i] = st[t-1]+1; //第一個比他小的位置+1,保證項目裏的高度>=a[i]
else lmin[i] = 0;
st[t++] = i;
}
AC代碼:
#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<stack>
#include<queue>
#include<ctime>
#include<map>
#define ll long long
#define ld long double
#define ull unsigned long long
using namespace std;
const ll mod = 998244353;
const int maxn = 101001;
int lmin[maxn],rmin[maxn];
int a[maxn],st[maxn];
int main(void)
{
int n;
while(scanf("%d",&n)&&n){
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
int t = 0;
for(int i=0;i<n;i++){
while(t&&a[st[t-1]]>=a[i])
t--;
if(t) lmin[i] = st[t-1]+1;
else lmin[i] = 0;
st[t++] = i;
}
t = 0;
for(int i=n-1;i>=0;i--){
while(t&&a[st[t-1]]>=a[i])
t--;
if(t) rmin[i] = st[t-1]-1;
else rmin[i] = n-1;
st[t++] = i;
}
ll ans = 0;
for(int i=0;i<n;i++){
ans = max(ans,(ll)(rmin[i]-lmin[i]+1)*a[i]);
}
printf("%lld\n",ans);
}
return 0;
}
單調隊列
一、定義
單調隊列:隊列中的元素之間的關係具有單調性,而且,隊首和隊尾都可以進行出隊操作,只有隊尾可以進行入隊操作。
二、用途
- 對於維護好的單調隊列,隊內的元素是有序的,那麼取出最大值(最小值)的時間複雜度是O(1)。
- 可以拿來維護DP
例題:POJ 2823
給定一個大小已知的數組以及一個大小已知的滑動窗口,窗口每個小時刻向後移一位,求出每個時刻窗口中數字的最大值和最小值。(n<=1e6)
Sample Input
8 3
1 3 -1 -3 5 3 6 7
Sample Output
-1 -3 -3 -3 3 3
3 3 5 5 6 7
如何解?如何解?
區間最值問題:我們想到了啥?線段樹?RMQ? O(nlogn)
新姿勢!!單調隊列!(O(n))!!!
以最小值爲例:
- 我們維護一個單增的隊列,那麼每次華東窗口入隊以後就要得到一個區間的最小值,按照剛纔的理論,直接取隊首元素就可以了??
- 具體做法:每次華東串口要得到最小值的時候。判斷隊首元素是否過期,如果過期,那就從隊首出隊,繼續找。
- 最大值做法同上:維護一個單減隊列。
void get_min()
{
int tail=0,head=1;
for(int i=1;i<m;i++){
while(head<=tail&&q[tail]>=a[i])
tail--;
q[++tail] = a[i];
p[tail] = i;
}
for(int i=m;i<=n;i++){
while(head<=tail&&q[tail]>=a[i])
tail--;
q[++tail] = a[i];
p[tail] = i;
while(p[head]<i-m+1)
++head;
amin[i-m+1] = q[head];
}
}
AC代碼:
#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<cctype>
#include<vector>
#include<stack>
#include<queue>
#include<ctime>
#include<map>
#define ll long long
#define ld long double
#define ull unsigned long long
using namespace std;
const ll mod = 998244353;
const int maxn = 1010001;
int a[maxn],amin[maxn],amax[maxn];
int q[maxn],p[maxn];
int n,m;
void get_min()
{
int tail=0,head=1;
for(int i=1;i<m;i++){
while(head<=tail&&q[tail]>=a[i])
tail--;
q[++tail] = a[i];
p[tail] = i;
}
for(int i=m;i<=n;i++){
while(head<=tail&&q[tail]>=a[i])
tail--;
q[++tail] = a[i];
p[tail] = i;
while(p[head]<i-m+1)
++head;
amin[i-m+1] = q[head];
}
}
void get_max()
{
int tail=0,head=1;
for(int i=1;i<m;i++){
while(head<=tail&&q[tail]<=a[i])
tail--;
q[++tail] = a[i];
p[tail] = i;
}
for(int i=m;i<=n;i++){
while(head<=tail&&q[tail]<=a[i])
tail--;
q[++tail] = a[i];
p[tail] = i;
while(p[head]<i-m+1)
++head;
amax[i-m+1] = q[head];
}
}
int main(void)
{
while(~scanf("%d%d",&n,&m)){
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
get_min();
for(int i=1;i<=n-m+1;i++){
printf("%d ",amin[i]);
}
puts("");
get_max();
for(int i=1;i<=n-m+1;i++){
printf("%d ",amax[i]);
}
puts("");
}
return 0;
}