题解 P5788 【【模板】单调栈】

· · 题解

update:2019/12/13

update:2020/2/17 $\ \ \ \ \ \ \ $修改了一点错别字,改了一下内容和定义。造成的曲解抱歉了。修改了伪代码使其规范。修改了题解规范。 # 单调栈 ## 前言 $\ \ \ \ \ \ \ $也是晚上突然看到新增加的模板。想着没学多久,想来发篇博客,熟悉一下。 ## 感谢 [lucky52529的博客](https://blog.csdn.net/lucky52529/article/details/89155694) ## 定义 $\ \ \ \ \ \ \ $我们都知道单调队列,也就是在这个队列里面存放的数据应该是有序的。当然,单调栈从这个冷艳的名字就可以看出来,这个栈存放的数据内部对应的顺序也应该是有序的。 $\ \ \ \ \ \ \ $我们根据存放下标对应元素的顺序,可以分为两种单调栈: - 单调递减栈,指的就是栈内存放下标对应元素构成的序列对应的元素单调递减。 - 单调递增栈,指的就是栈内存放下标对应元素构成的序列对应的元素单调递增。 $\ \ \ \ \ \ \ $**一定注意,我们存放的一般是下标,而不是元素。但是作为比较的标准是下标对应的元素**。 ## 模拟 $\ \ \ \ \ \ \ $让我们模拟一遍单调栈的运行过程,这里的单调栈是单调递增栈。 $\ \ \ \ \ \ \ $我们现在有 $6$ 个数 $5,2,3,4,1,6$。 $\ \ \ \ \ \ \ $首先我们要明白,我们存放的是下标。然后,我们直接把元素放在栈顶,会破坏它的单调性。所以我们需要吐出栈顶的元素,使得我们当前的元素再加进去不会破坏它的单调性。 - 我们当前栈内没有元素,将 $5$ 加入。现在栈内元素应该是 $1$。 - 当前元素为 $2$,我们发现加入之后不能单调,于是吐出 $5$,加入 $2$。当前栈内元素为 $2$。 - 接下来是 $3,4$。我们发现加入不会破坏单调性,于是直接加入,栈内元素 $2,3,4$。 - 遇到 $1$,只能吐出栈内所有元素,加入 $1$。 - 最后加入 $6$。整个算法流程完成。 ## 伪代码 $\ \ \ \ \ \ \ $我们可以根据这个算法流程,打出伪代码。 ```cpp stack<int> S; for i←1 to n if S.size=0 || a[S.top]>=a[i] then S.push i else while S.size && a[S.top]<a[i] do S.pop end S.push i ``` $\ \ \ \ \ \ \ $我猜是这样,毕竟没学过伪代码。 ## 杂七杂八的问题 $\ \ \ \ \ \ \ $对于我自己来说,我学的时候会有一些问题,在这里带着自己的经验解释一下。 1. 单调栈解决的主要问题是什么呢? $\ \ \ \ \ \ \ $就跟单调队列差不多。单调队列主要处理的是一个区间内的最大/小值,而单调栈处理的是寻找以某个值为最小/大值的最大区间。相比较,实际上单调栈用的虽然少一些,但是比单调队列更加灵活多变。 2. 为什么单调栈是正确的呢? $\ \ \ \ \ \ \ $对于这道题来说,我们定义一个元素失效,当且仅当这个元素的 $f_i$ 被保存。我们假设将栈中已有元素。既然栈中元素没有被弹出,那么证明还没有遇到比它大的元素。当我们的元素从栈中弹出的时候,这证明了它发现了第一个比它还要大的数,这道题刚好满足,于是保存 $f_i$ ,继续算法流程。同理,对于之前的主要问题,我们找到了一个比它还要大的数,说明这个区间结束了。 3. 单调栈的时间复杂度是? $\ \ \ \ \ \ \ $想一下,我们的每一个元素最多进栈/出栈一次,所以说时间复杂度 $O(n)$。 $\ \ \ \ \ \ \ $**下文所说元素可能不是指的下标对应元素而是直接指元素,没有提示请不要混淆。** ## 结束标识符 $\ \ \ \ \ \ \ $对于某些特殊题目,栈内元素出不完,会导致答案错误,这时候我们要添加结束标识符强制吐出所有栈内元素。 $\ \ \ \ \ \ \ $找个例子: ### 视野总和 $\ \ \ \ \ \ \ $[lucky52529的博客](https://blog.csdn.net/lucky52529/article/details/89155694),自查第一题。 $\ \ \ \ \ \ \ $我们很容易想到构造一个单调递增栈,如果遇到大于栈顶的元素,开始更新之前不高于当前人所能看到的值即可。 $\ \ \ \ \ \ \ $但是我们发现我们 WA 了,因为遗留在栈里面的人还没有计算贡献。我们于是用结束标识符,这里是极大值,想象一个无限高的人站在最右边,那么我们所有人都能出栈,不会漏掉。 #### 代码 ```cpp #include<cstdio> #include<stack> #include<algorithm> #define mp make_pair using namespace std; stack<long long> S; long long a[5000005],ans; int main(){ long long n; scanf("%lld",&n); for(long long i=1;i<=n;++i) scanf("%lld",&a[i]); a[n+1]=10086001100860011ll;//结束标识符 for(long long i=1;i<=n+1;++i) { if(S.empty() || a[S.top()]>=a[i]) S.push(i); else { while(S.size() && a[S.top()]<a[i]) { long long Top=S.top(); ans+=(i-Top-1); S.pop(); } S.push(i); } } printf("%lld",ans); return 0; } ``` ### 直方图中最大的矩形 $\ \ \ \ \ \ \ $[lucky52529的博客](https://blog.csdn.net/lucky52529/article/details/89155694),自查第二题。 $\ \ \ \ \ \ \ $上面都是用的单调递增栈,这次我们用单调递减栈。我们每次将元素从栈里面弹出的时候,因为我们的答案可能会出现在里面,所以我们弹出元素就计算一遍答案。 #### 代码 ```cpp #include<cstdio> #include<algorithm> #include<iostream> #include<stack> #include<queue> using namespace std; long long a[100005],n; stack<long long> S; void debug() { stack<long long> tmp=S; while(tmp.size()) printf("%lld ",tmp.top()),tmp.pop(); puts(""); } void Clear() { stack<long long> tmp; S=tmp; } int main(){ while(scanf("%lld",&n) && n) { Clear(); long long ans=0; for(long long i=1;i<=n;++i) scanf("%lld",&a[i]); a[n+1]=-2147483647; for(long long i=1;i<=n+1;++i) { if(S.empty() || a[S.top()]<=a[i]) S.push(i); else { long long Top=0; while(S.size() && a[S.top()]>a[i]) { // debug(); Top=S.top(); ans=max(ans,(i-Top)*a[Top]); S.pop(); } a[Top]=a[i];//why? S.push(Top); } } printf("%lld\n",ans); } return 0; } ``` $\ \ \ \ \ \ \ $发现我在 `a[Top]=a[i];` 这个语句后打了个 `//why?`。这是为什么呢? $\ \ \ \ \ \ \ $我们弹出元素计算答案,相信大家都能够理解。但是为什么要更改我们的 $a$ 数组呢? $\ \ \ \ \ \ \ $举个例子:$2,3,1$。 $\ \ \ \ \ \ \ $思考一下,我们要维持它单调,假设加入 $3$ 这个元素的时候栈只有它一个元素。我们的 $top$ 会从 $1$ 变成 $3$。但是 $1,2$ 两个元素对应的值都比它大。但是我们为了保持栈中的递增属性,并且可以让向左拓展,我们索性修改了 $i$ 的下标,将他修改为最左边的 $top$ 下标,所以当我们下次需要以他为基准获取矩形面积。 ### 题解 $\ \ \ \ \ \ \ $对于这道题,我们构造一个单调递增栈,我们可以想象,我们每次出栈,都是因为找到了一个比当前栈顶元素大的元素,可以证明它一定是最早出现的,于是用数组记录答案,每次出栈保存答案。注意卡常。 ```cpp #include<cstdio> int a[3000005],f[3000005],S[3000005],n; int read() { int x=0,f=1; char c=getchar(); while(c<'0' || c>'9') { if(c=='-') f=-1; c=getchar(); } while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^'0'),c=getchar(); return x*f; } void write(int x) { if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); } int main(){ n=read(); for(int i=1;i<=n;++i) a[i]=read(); int top=0;//用数组模拟栈,不然会T for(int i=1;i<=n;++i) { while(top && !(a[S[top]]>=a[i])) f[S[top--]]=i;//我们发现直接加入会破坏单调性,于是弹出栈顶元素,顺便计算当前元素的答案 S[++top]=i; } for(int i=1;i<=n;++i) write(f[i]),putchar(' '); return 0; } ``` ## 例题 $\ \ \ \ \ \ \ $[AT1225](https://www.luogu.com.cn/problem/AT1225),CDQ分治套单调栈,较难。 $\ \ \ \ \ \ \ $[lucky52529的博客](https://blog.csdn.net/lucky52529/article/details/89155694),自查第三题。