單調佇列與單調棧用法詳解
基本資料結構的應用一 棧和佇列
單調棧 單調佇列 和 優先佇列的應用
1.單調棧
單調棧是指一個棧內部的元素是具有嚴格單調性的一種資料結構,分為單調遞增棧和單調遞減棧。
單調棧有兩個性質
1.滿足從棧頂到棧底的元素具有嚴格的單調性
2.滿足棧的後進先出特性越靠近棧底的元素越早進棧
元素進棧過程
對於一個單調遞增棧來說 若當前進棧的元素為 a 如果a < 棧頂元素則直接將a 進棧 如果 a >= 當前棧頂元素則不斷將棧頂元素出棧知道滿足 a < 棧頂元素
模擬一個數列構造一個單調遞增棧
進棧元素分別為3,4,2,6,4,5,2,3。
首先 3 進棧 變為 {3};
然後 4 進棧 先讓 3 出棧 再進棧 變為 {4};
2 進棧 變為 {4,2};
對於 6 先讓 4,2 出棧 再讓 6 進棧 {6};
4 進棧 {6,4};
讓4 出棧 5進棧 {6,5};
然後2進 2出 3進 變為 {6,5,3};
由於單調棧只能對棧頂位置進行操作 所以一般應用於只有對陣列有一邊有限制的地方
實現用STL的棧實現即可 模擬棧也行
2.單調佇列
單調佇列是指一個佇列內部的元素具有嚴格單調性的一種資料結構,分為單調遞增佇列和單調遞減佇列。
單調佇列滿足兩個性質
1.單調佇列必須滿足從隊頭到隊尾的嚴格單調性。
2.排在佇列前面的比排在佇列後面的要先進隊。
元素進佇列的過程對於單調遞增佇列,對於一個元素a 如果 a > 隊尾元素 那麼直接將a扔進佇列 如果 a <= 隊尾元素 則將隊尾元素出佇列 知道滿足 a 大於隊尾元素即可;
實現用 stl 的雙端佇列即可。
由於雙端佇列即可以在隊頭操作也可以在隊尾操作那麼這樣的性質就彌補了單調棧只能在一遍操作的不足可以使得其左邊也有一定的限制。
question 1:給你n個正數一個區間的權值定義為這個區間的所有數之和乘以這個區間內最小的數求所有區間中權值最大的區間。
solve : 對於這個題目我們不難想到要看每一個數作為最小值(或者最大值)的最長的區間是什麼,對於這一類問題我們可以考慮用單調棧或者單調佇列 用單調遞減棧維護一下每次每個數的最大影響區間就是棧的前一個數到下一個要使得這個數出棧的位置。可以將棧中的每一個數看做一堵牆過了這個牆最小值就變成了牆上的這個數。
AC code :
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <stack>
using namespace std;
const int maxn = 1e5 + 10;
struct node{
long long num;
int pos;
};
node a[maxn];
long long sum[maxn] = {0};
int main(){
int n;
scanf("%d",&n);
int l = 1;
int r = n;
long long ans = 0;
for(int i = 1;i <= n; ++ i){
scanf("%lld",&a[i].num);
sum[i] = sum[i - 1] + a[i].num;
a[i].pos = i;
}
stack <node> st;
for(int i = 1;i <= n; ++ i){
if(st.empty()){
st.push(a[i]);
continue;
}
while(!st.empty() && st.top().num >= a[i].num){
node u = st.top();
st.pop();
int j;
if(!st.empty())
j = st.top().pos;
else{
j = 0;
}
long long temp = (sum[i - 1] - sum[j]) * u.num;
if(temp > ans){
ans = temp;
l = j + 1;
r = i - 1;
}
// cout << temp << endl;
}
st.push(a[i]);
}
int rr = n,ll = 1;
while(!st.empty()){
node u = st.top();
st.pop();
if(st.empty()){
ll = 0;
}
else{
ll = st.top().pos;
}
long long temp = (sum[rr] - sum[ll]) * u.num;
if(temp > ans){
ans = temp;
l = ll + 1;
r = rr;
}
// cout << temp << endl;
}
printf("%lld\n%d %d\n",ans,l,r);
return 0;
}
question 2 : 求柵欄矩形的最大面積
這個問題和上面那個問題十分類似 我們也只需要一個單調棧 找到一個數最大的影響區間就可以了。還是用遞減單調棧。
question 3 : POJ 3017
將一個由N個非負整陣列成的序列劃分成若干段,要求每段數字 的和不超過M,求每段的最大值的和最小的劃分方法,輸出這個 最小的和
solve : 對於這個問題我們不難想到一個狀態為一維轉移為o(n)的 1D/1D方程 但是 時間複雜度為 O(n ^ 2)的演算法是時間複雜度不能接受的。
dp(i) 表示前i個劃分成若干段的最大值的和最小。
dp[i] = min(dp[j] + maxnum{A[j + 1]...A[i]}) ;
對於這個方程我們不難發現 dp(i)是單調遞增的 然後可以發現maxnum是從右向左單調遞增的這個時候就可以使用一個單調佇列進行維護
假設x為A[j + 1]...A[i]最大值的位置,即 • A[x] = maxnum{A[j + 1]...A[i]}
•則,對於任意y滿足y >= j && y < x • maxnum{A[y + 1]...A[i]}為定值
- 又,A[i]非負,則dp[i]單調不減
• 綜上,在求dp[i]時,對於最大值相等的區間應取最前面的
所以我們就可以用單調佇列加上 mutiset 將其優化到 nlogn級別 就可以ac了
AC code :
//#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int maxn = 1e5 + 10;
long long a[maxn] = {0};
long long sum[maxn] = {0};
int n,m;
long long dp[maxn] = {0};
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= n; ++ i){
scanf("%lld", &a[i]);
sum[i] = sum[i-1] + a[i];
}
for(int i = 1;i <= n; ++ i)
dp[i] = 10000000000000007;
dp[0] = 0;
dp[1] = a[1];
long long mm = a[1];
deque <int> dq;
dq.push_back(1);
for(int i = 2;i <= n; ++ i){
mm = max(mm,a[i]);
while(!dq.empty() && a[i] >= a[dq.back()]){
dq.pop_back();
}
dq.push_back(i);
while(!dq.empty() && sum[i] - sum[dq.front() - 1] > m)
dq.pop_front();
int l = lower_bound(sum + 1,sum + n + 1, sum[i] - m) - sum;
if(sum[i] - sum[l-1] > m)
l ++;
int k = dq.size();
for(int j = 0;j < k; ++ j){
dp[i] = min(dp[i],dp[l - 1] + a[dq.front()]);
int u = dq.front();
l = u + 1;
dq.pop_front();
dq.push_back(u);
}
}
if(mm > m)
printf("-1\n");
else
printf("%lld\n",dp[n]);
return 0;
}