下面引自百度百科
单调递增或单调减的栈,跟单调队列差不多,但是只用到它的一端,利用它可以用来解决一些ACM/ICPC和OI的题目,如RQNOJ 的诺诺的队列等。
单调栈是一种特殊的栈,特殊之处在于栈内的元素都保持一个单调性。
假设下图是一个栈内元素的排列情况(单调递增的栈):
此时插入情况有两种:
(1).插入元素大于栈顶元素
当插入7时,因7 > 6,满足单调递增的条件,故可以直接加入栈
此时:
(2).插入的元素小于栈顶元素
当插入3时,为了满足单调递增栈的性质,需要先将栈顶的4,6弹出,再插入,此时:
以上的内容和图我相信是非常容易理解的,但单调栈的作用和功能并不能得到很好的体现,故下面将用文字 + 图示的形式来展示单调栈的作用
先上结论:
利用单调栈,可以找到从左/右遍历第一个比它小/大的元素的位置
举个例子:
假设有一个单调递增的栈 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
总结:一个元素向左遍历的第一个比它小的数的位置就是将它插入单调栈时栈顶元素的值,若栈为空,则说明不存在这么一个数。然后将此元素的下标存入栈,就能类似迭代般地求解后面的元素
代码:
Stack<int> S;
for(int i=1 ;i<=n ;i++){
while(S.size() && a[S.top()] >= a[i]) S.pop();
if(S.empty()) L[i] = 0;
else L[i] = S.top();
S.push(i);
}
看到这里我相信你一定会有疑问,不知道这个功能有什么作用。
但其实通过下面的例题你会发现,用好单调栈,我们就可以解决一些看似非常复杂的问题。
题目链接:
首先考虑最大面积的矩形X的左右边界的性质:
设其左边界为L,右边界为R,则其高H = min{h[i] | L <= i <= R}
此时最大面积为 (R - L + 1) * H
若此时左边界的左边那个矩形的高度 h[L-1] >= H
则左边界可以向左拓展,则新的面积为:
(R - (L-1) + 1) * H > 原面积
则与原假设条件冲突
故左边界左边的那个矩形的高度 :h[L-1] < H
同理右边界右边的那个矩形的高度: h[R+1] < H
设H = h[i]
所以左边界L是满足h[j-1] < h[i]的最大的j,即从i点向左遍历的第一个高度比i小的点的右边一个点
而右边界R是满足 h[j+1] < h[i]的最小的j,即从i点向右遍历第一个高度比i小的点的左边一个点
所以我们可以利用单调栈的性质得到每个确定点,即确定高度的最大面积矩形的左右边界,然后枚举取最大即可。
代码:
#include
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define rep(i, a, b) for(int i(a); i <= (b); ++i)
#define dec(i, a, b) for(int i(a); i >= (b); --i)
#define MP make_pair
const int N = 100000 + 100;
stack<int> S;
ll h[N];
int R[N],L[N];
int main(){
int n;
while(~scanf("%d",&n) && n){
for(int i=0 ;iscanf("%I64d",&h[i]);
while(S.size()) S.pop();
for(int i=0 ;iwhile(S.size() && h[S.top()] >= h[i]) S.pop();
if(S.empty()) L[i] = 0;
else L[i] = S.top() + 1;
S.push(i);
}
while(S.size()) S.pop();
for(int i=n-1 ;i>=0 ;i--){
while(S.size() && h[S.top()] >= h[i]) S.pop();
if(S.empty()) R[i] = n;
else R[i] = S.top();
S.push(i);
}
ll ans = 0;
for(int i=0 ;iprintf("%I64d\n",ans);
}
return 0;
}
题目链接
这是北京区域赛的一道题,确实才接触时感觉难度很大,想了两天 加 在网上查看了题解才真正AC。这道题让我对单调栈的理解加深了不少。
题意不难理解,但难在如何利用单调栈的性质快速求解。
之前的想法是一次初始化,二分查询再跳跃式地查找区间左右边界点。
但奈何复杂度非常高,此题是明显地过不了的。
所以正确需要离线处理,先读入所有的查询,将每个查询点视为高度为0的楼,然后再通过比较两栋相邻楼顶连线的斜率大小维护一个单调栈。
一些细节性的东西网上有很多题解,便不再赘述。
#include
using namespace std;
const int A = 100000 + 100;
const double PI = acos(-1.0);
const double eps = 1e-8;
class Build{
public:
int id;
double x,h;
friend bool operator<(Build &a,Build &b){
return b.x > a.x;
}
};
Build build[A*2];
double R[2*A],L[2*A];
int S[A*2];
int num,cnt,q,n;
int dcmp(double x){
return x < -eps ? -1 : x > eps;
}
double f(int i,int j,int type){
if(type == 1)
return (build[i].h - build[j].h) / (build[j].x - build[i].x);
return (build[j].h - build[i].h) / (build[j].x - build[i].x);
}
inline void solve(){
cnt = 0;
for(int i=0 ;iwhile(cnt >= 2 && f(S[cnt-1],S[cnt],1) - f(S[cnt],i,1) >= 0) {
cnt--;
}
if(build[i].id >= 0){
if(cnt == 0) L[build[i].id] = 0.0;
else L[build[i].id] = atan(f(S[cnt],i,1)) / (2.0*PI) * 360.0;
}
S[++cnt] = i;
}
cnt = 0;
for(int i=num-1 ;i >= 0;i--){
while(cnt >= 2 && f(S[cnt],S[cnt-1],2) - f(i,S[cnt],2) >= 0){
cnt--;
}
if(build[i].id >= 0){
if(cnt == 0) R[build[i].id] = 0;
else R[build[i].id] = atan(f(i,S[cnt],2)) / (2.0*PI) * 360.0;
}
S[++cnt] = i;
}
for(int i=0 ;iprintf("%.10f\n",180.0 - (L[i] + R[i]));
}
}
int main(){
int T,_=1;
scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i=0 ;iscanf("%lf%lf",&build[i].x,&build[i].h);
build[i].id = -1;
}
scanf("%d",&q);
for(int i=n ;i < n+q ;i++){
scanf("%lf",&build[i].x);
build[i].h = 0;
build[i].id = i - n;
}
num = n + q;
sort(build,build+num);
printf("Case #%d:\n",_++);
solve();
}
return 0;
}
题目链接
题意很好理解,但此题不仅要求区间的边界点,还要求区间的和,所以用一个树状数组维护一个区间和即可。
另外注意全部为一个值和最大值为0的情况。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int A = 1e5 + 100;
int n,cnt;
int a[A];
int S[A],L[A],R[A];
ll Tree[A];
int lowbit(int x){
return x & (-x);
}
ll getsum(int pos){
ll res = 0;
while(pos >= 1){
res += Tree[pos];
pos -= lowbit(pos);
}
return res;
}
void add(int pos,int val){
while(pos <= n){
Tree[pos] += val;
pos += lowbit(pos);
}
}
void solve(){
cnt = 0;
for(int i=1 ;i<=n ;i++){
while(cnt > 0 && a[S[cnt]] >= a[i]) cnt--;
if(cnt == 0) L[i] = 1;
else L[i] = S[cnt] + 1;
S[++cnt] = i;
//printf("%d : L = %d\n",i,L[i]);
}
cnt = 0;
for(int i=n ;i>=1 ;i--){
while(cnt > 0 && a[S[cnt]] >= a[i]) cnt--;
if(cnt == 0) R[i] = n;
else R[i] = S[cnt] - 1;
S[++cnt] = i;
// printf("%d : R = %d\n",i,R[i]);
}
ll ans = 0;
int left,right;
for(int i = 1;i<=n ;i++){
ll sum = getsum(R[i]) - getsum(L[i]-1);
sum *= a[i];
if(sum >= ans){
left = L[i];
right = R[i];
ans = sum;
}
}
printf("%lld\n",ans);
printf("%d %d\n",left,right);
}
int main(){
while(~scanf("%d",&n)){
memset(Tree,0,sizeof(Tree));
for(int i=1 ;i<=n ;i++){
scanf("%d",&a[i]);
add(i,a[i]);
}
a[0] = 0;
solve();
}
return 0;
}
题目链接
很裸的单调栈,而且只需要跑一个方向。
简单入门题
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int INF = 1e9 + 7;
const int A = 1e5 + 10;
int a[A],S[A],num[A];
int cnt,n;
void solve(){
cnt = 0;
a[n] = INF;
for(int i=n ;i>=0 ;i--){
while(cnt>0 && a[S[cnt]] < a[i]) cnt--;
if(cnt == 0) num[i] = 0;
else num[i] = S[cnt] - i - 1;
S[++cnt] = i;
}
ll ans = 0;
for(int i=0 ;iprintf("%I64d\n",ans);
}
int main(){
scanf("%d",&n);
for(int i=0 ;iscanf("%d",&a[i]);
}
solve();
return 0;
}