單調棧
定義:
單調棧,顧名思義,是棧內元素保持一定單調性(單調遞增或單調遞減)的棧。這裏的單調遞增或遞減是指的從棧頂到棧底單調遞增或遞減。既然是棧,就滿足後進先出的特點。與之相對應的是單調隊列。
實現:
先上結論:
利用單調棧,可以找到從左/右遍歷第一個比它小/大的元素的位置
一:
舉個例子:
假設有一個單調遞增的棧 S和一組數列:
a : 5 3 7 4
用數組L[i] 表示 第i個數向左遍歷的第一個比它小的元素的位置
如何求L[i]?
首先我們考慮一個樸素的算法,可以按順序枚舉每一個數,然後再依此向左遍歷。
但是當數列單調遞減時,複雜度是嚴格的O(n^2)。
此時我們便可以利用單調棧在O(n)的複雜度下實現
我們按順序遍歷數組,然後構造一個單調遞增棧
(1). i = 1時,因棧爲空,L[1] = 0,此時再將第一個元素的位置下標1存入棧中
此時棧中情況:
(2).i = 2時,因當前3小於棧頂元素對應的元素5,故將5彈出棧
此時棧爲空
故L[2] = 0
然後將元素3對應的位置下標2存入棧中
此時棧中情況:
(3).i = 3時,因當前7大於棧頂元素對應的元素3,故
L[3] = S.top() = 2 (棧頂元素的值)
然後將元素7對應的下標3存入棧
此時棧中情況:
(4).i = 4時,爲保持單調遞增的性質,應將棧頂元素3彈出
此時 L[4] = S.top() = 2;
然後將元素4對應的下標3存入棧
此時棧中情況:
至此 算法結束
對應的結果:
a : 5 3 7 4
L : 0 0 2 2
二:
例如實現一個單調遞增的棧,比如現在有一組數10,3,7,4,12。從左到右依次入棧,則如果棧爲空或入棧元素值小於棧頂元素值,則入棧;否則,如果入棧則會破壞棧的單調性,則需要把比入棧元素小的元素全部出棧。單調遞減的棧反之。
10入棧時,棧爲空,直接入棧,棧內元素爲10。
3入棧時,棧頂元素10比3大,則入棧,棧內元素爲10,3。
7入棧時,棧頂元素3比7小,則棧頂元素出棧,此時棧頂元素爲10,比7大,則7入棧,棧內元素爲10,7。
4入棧時,棧頂元素7比4大,則入棧,棧內元素爲10,7,4。
12入棧時,棧頂元素4比12小,4出棧,此時棧頂元素爲7,仍比12小,棧頂元素7繼續出棧,此時棧頂元素爲10,仍比12小,10出棧,此時棧爲空,12入棧,棧內元素爲12。
至於代碼的實現我覺得還是必須對應着題目去體會,也沒有太死板的模板,下面只給出僞代碼吧。
/*
* 本僞代碼對應的是單調遞減棧
*共n個元素,編號爲0~n-1
*/
while(棧爲空) 棧頂元素出棧; //先清空棧
a[n]=-1;
for(i=0;i<=n;i++)
{
if(棧爲空或入棧元素大於等於棧頂元素) 入棧;
else
{
while(棧非空並且棧頂元素大於等於入棧元素)
{
棧頂元素出棧;
更新結果;
}
將最後一次出棧的棧頂元素(即當前元素可以拓展到的位置)入棧;
更新最後一次出棧的棧頂元素其對應的值;
}
}
輸出結果;
將破壞棧單調性的元素都出棧後,最後一次出棧的元素就是當前入棧元素能拓展到的最左位置,更新其對應的值,並將其位置入棧。(注: 對於這句話到現在都沒有理解, 如果有大佬路過可以幫我解釋解釋。)
應用:
以上就是一個單調棧的定義及其實現,下面就來說一下它可以解決哪些問題。其實我也不能給出證明,以證明它爲什麼能完成這些功能,只是簡單的把它的用途說一下,碰到問題時就需要大家靈活運用了。
1.最基礎的應用就是給定一組數,針對每個數,尋找它和它右邊第一個比它大的數之間有多少個數。
2.給定一序列,尋找某一子序列,使得子序列中的最小值乘以子序列的長度最大。
3.給定一序列,尋找某一子序列,使得子序列中的最小值乘以子序列所有元素和最大。
對應題目:做的還不多,以後慢慢增加。
1、完美矩陣;
這題選拔賽做的時候沒有寫出來,主要因爲太菜, 想到用單調棧,但沒有考慮的很全面,先說說我看到的第一感覺,用貪心,在每一層以高度遞增遍歷每一層,問題出在當時找到連續的就break了,沒有繼續往後找,不過以數據大小來說這樣寫一定會TLE的,之後想到單調棧,但以高度入棧了,寫的一團亂。直到學長講解的時候思路纔打開,應該以下標入棧。分別在左右找他的第一個小於他的數。就A了。這個實現不難就不貼下來了。後面再網上看到了兩個寫法,思路大致相同不過各有優劣。
-找出每一個單位寬度矩形的左邊界 l 和右邊界 r ,左邊界定義爲左邊連續的高度大於等於它的最左邊的矩形的下標,右邊界同理,從左往右推出所有的左邊界,從右往左推出所有的右邊界。
注意:矩形的高度h可以等於零,以爲這當左邊邊上的矩形高爲零或者右邊邊界上矩形高度爲零時,while循環無法停止,所以在while循環的條件中要加上限制邊界的條件。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define max(a, b) ((a > b) ? (a) : (b))
long long h[100010],l[100010],r[100010];
int main()
{
int n;
scanf("%d",&n);
int i, t;
for(i=1; i<=n; i++)
{
scanf("%lld",&h[i]);
}
memset(l, 0, sizeof(l));
memset(r, 0, sizeof(r));
l[1]=1;
r[n]=n;
for(i=2; i<=n; i++)
{
t=i;
while(t>1&&h[i]<=h[t-1])
t=l[t-1];
l[i]=t;
}
for(i=n-1; i>0; i--)
{
t=i;
while(t<n&&h[i]<=h[t+1])
t=r[t+1];
r[i]=t;
}
long long s=0;
for(i=1; i<=n; i++)
{
s = max(s, (r[i]-l[i]+1)*h[i]);
}
printf("%lld\n",s);
return 0;
}
- 利用根據單調遞增棧解決,如果棧爲空或入棧元素小於棧頂元素則入棧,否則會破壞棧的單調性,則將棧頂元素出棧並更新結果,直到棧爲空或碰到一個小於入棧元素的值。然後將當前元素入棧。這個方法還沒有徹底掌握,先貼代碼。
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<stack>
using namespace std;
typedef long long LL;
int main()
{
int i,n,top; //top指向棧頂
stack<int> st; //棧用於保存矩形的編號,即位置
LL tmp,ans,a[100010]; //tmp爲臨時變量,記錄面積的值,ans爲結果,記錄最大面積值
while(~scanf("%d",&n)&&n)
{
for(i=0;i<n;i++)
scanf("%lld",&a[i]);
ans=0;
a[n]=-1; //最後一個元素設爲最小值,以最後清空棧
for(i=0;i<=n;i++)
{
if(st.empty()||a[i]>=a[st.top()])
{ //如果棧爲空或入棧元素大於等於棧頂元素 ,則入棧
st.push(i);
}
else
{
while(!st.empty()&&a[i]<a[st.top()])
{ //如果棧非空且入棧元素小於棧頂元素,則將棧頂元素出棧
top=st.top();
st.pop();
tmp=(i-top)*a[top]; //在出棧過程中計算面積值
if(tmp>ans) ans=tmp; //更新面積最大值
}
st.push(top); //只將可以延伸到的最左端的位置入棧
a[top]=a[i]; //並修改該位置的值
}
}
printf("%lld\n",ans);
}
return 0;
}