单调栈
我们在上一节中初步学习了栈这种数据结构的算法思路,在本节中我们将会一起学习栈的一种特殊形式--单调栈。 单调栈,顾名思义其中的元素是单调递增或单调递减的。普通的栈该如何转变为单调栈呢?以从底到顶递减为例:我们按顺序压入元素,如果新的元素比栈顶元素小,就入栈;如果新的元素较大,那就一直把栈内元素弹出,直到栈顶比心元素小,新元素入栈。
元素间大小判断: 对于当前遍历到的数,栈顶为答案。(入栈时记录答案) 1:向左找第一个比自己大的数。 左--->右 底到顶递减栈 (1)如果a[i]入栈时,栈空,说明a[i]左边没有比它大的数 (2)用递减栈来保存已经遍历过的数,当枚举到第i个数时,栈中的数就是可能比第i个数大的数。这个数和栈顶比较,栈顶比i大,栈顶就是答案,否则就连续出栈,直到一个栈顶比i大,此时记录答案,再将i入栈
但是,在遍历过程中会有一些元素出栈,这种方法不会导致漏掉答案吗? 其实不会,因为我们出栈的数x1,x2,x3...都是比当时新的栈顶y小的数,那么现在遍历到i,假设x1,x2,x3。y > 1,由于y离i更近,所以x1,x2,x3一定不会是答案,所以即便它们不在栈中也无妨。 比如:2 7 3 6 1,我们向左找比1大的第一个数,此时栈为 7 6 ,1比2,3小,那么1一定比6小,那么答案就是6。
2:向左找第一个比自己小的数。 左--->右 底到顶递增栈
3:向右找第一个比自己大的数。 右--->左 底到顶递减栈
4:向右找第一个比自己小的数。 右--->左 底到顶递增栈
下面是对第一种情况的代码示例:
#include <iostream>
#include <cmath>
#include <string>
using namespace std;
//如果不存在答案,输出-1
int n;//n <= 100
int a[105];
stack<int> s;
int ans[105];//ans[i]就是a[i]左边第一个比a[i]大的数
int main(){
cin >> n;
for(int i = 1; i <= n; ++i){
cin >> a[i];
}
for(int i = 1; i <= n; ++i){
//栈非空并且当前栈顶小于a[i]--连续出栈
while(!s.empty() && s.top() <= a[i]){
s.pop();
}
//栈有可能空
if(s.empty()){
ans[i] = -1;
}
else{
ans[i] = s.top();
}
s.push(a[i]);//入栈时记录答案
}
for(int i = 1; i < n; ++i){
cout << ans[i] << " ";
}
return 0;
}
元素间大小判断: 对于栈顶,当前遍历到的数为答案。(出栈时记录答案) 1:向左找第一个比自己大的数。 右--->左 底到顶递减栈 (1)当栈为空时,直接入栈 (2)当遍历到a[i],如果a[i]大于栈顶,就要联系出栈,a[i]就是连续出栈的数的答案,栈顶出栈时记录栈顶的答案,否则a[i]直接入栈。 (3)当遍历完后,此时若栈非空,栈中的数据就没有答案,连续出栈,把答案记录为-1
2:向左找第一个比自己小的数。 右--->左 底到顶递增栈
3:向右找第一个比自己大的数。 左--->右 底到顶递减栈
4:向右找第一个比自己小的数。 左--->右 底到顶递增栈
下面是对第一种方法的代码示例:
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int n;
int a[105];
int ans[105];
stack<int> s;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
if (!(cin >> n)) return 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = n; i >= 1; --i) {
while (!s.empty() && a[s.top()] < a[i]) {
ans[s.top()] = a[i];
s.pop();
}
s.push(i);
}
while (!s.empty()) {
ans[s.top()] = -1;
s.pop();
}
for (int i = 1; i <= n; ++i) {
cout << ans[i] << (i == n ? "" : " ");
}
cout << endl;
return 0;
}
上面两种方法里,其实第一种(在入栈时记录答案)会更加方便理解,但是大家选择自己觉得好理解的方法就行。
下面我们来一起看一道题目~
分析: 这道题要找第i个元素之后第一个大于ai的元素下标,其实就是第三种情况:向右找第一个比自己大的元素的下标,即从右向左遍历,从底到顶的递减栈。
下面是代码示例:
#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
int n;
int a[3000005];
stack<int> s;
int ans[3000005];
int main(){
cin >> n;
for(int i = 1; i <= n; ++i){
cin >> a[i];
}
for(int i = n; i >= 1; --i){
while(!s.empty() && a[s.top()] <= a[i]){
s.pop();
}
if(s.empty()){
ans[i] = 0;
}
else{//a[s.top()] > a[i]
ans[i] = s.top();
}
s.push(i);
}
for(int i = 1; i <= n; ++i){
cout << ans[i] << " ";
}
}
下一道题目,是一道很经典的题目--接雨水,这道题的解法很多,有动态规划,也可以用单调栈解决,大家可以先思考一下自己最拿手的方法。
分析: 这道题目要求我们计算可以接到的雨水的总量,其实也就是算每一个横坐标单位下接到的雨水的量,然后计算总量即可。 那么怎么去计算每一个横坐标单位下的雨水呢?我们先讲一讲最大前后缀的解法: 对于一个位置,本身有一个柱子的高度,我们先向左去找最高的大于这个高度的柱子,再向右去找最高的大于这个高度的柱子,取两者的最小值(木板原理),再减去本身柱子的高度,就是这个位置可以接到的雨水的量。
比如下图中,待求左边第一个比1大的是2,右边第一个比1大的是3,如果我们取两根柱子高度的较大数,水就溢出来了,所以要取较小的柱子高度
下面是最大前缀+最大后缀解法的代码示例:
#include <iostream>
#include <vector>
using namespace std;
int n;
int trap(vector<int>& height) {
int leftMax[n];
int rightMax[n];
int res[n];
int sum = 0;
int temp = 0;
leftMax[0] = rightMax[n-1] = 0;
for(int i = 1; i < n; ++i){
leftMax[i] = max(leftMax[i-1], height[i-1]);
}
for(int i = n-2; i > -1; --i){
rightMax[i] = max(rightMax[i+1], height[i+1]);
}
for(int i = 0; i < n; ++i){
res[i] = min(leftMax[i], rightMax[i]);
temp = res[i] - height[i];
if(temp < 0){
temp = 0;
}
sum += temp;
}
return sum;
}
int main(){
vector<int> height;
int x;
cin >> n;
for(int i = 1; i <= n; ++i){
cin >> x;
height.push_back(x);
}
int ans = trap(height);
cout << ans << endl;
return 0;
}
好了,我们现在再看看单调栈的解法: 我们考虑构造一个自底向顶递减的单调栈,从左往右对每一个柱子的高度尝试push,如果尝试压入的数小于栈顶则压入,如果尝试压入的数大于栈顶,则在图中的表现则是出现了凹槽,需要进行凹槽内雨水量的计算。
为了方便大家理解,这里我们规定单调栈是不严格单调的,即遇到相同的元素不必出栈。 每次出栈或栈顶元素和待压入元素相等,都要取(“出栈元素前一个元素的高”和“待压入元素的高”的最小值 - 出栈元素的高)作为凹槽的壁高,并以“待压入元素的下标”和“出栈元素的前一个元素的下标”的差作为凹槽的底边长度。二者的乘积就是这个凹槽的雨水储量。
第一个元素和最后一个元素比较特殊,我们特别分析一下。我们以这道题中的图为例,当第二个元素准备入栈时,发现1比栈顶元素0更大,那么我们准备计算凹槽,但是0下面没有元素了,所以构不成凹槽壁,不纳入计算。而对最后一个元素,右侧没有元素,也是构不成凹槽,自然结束即可。
上图中,蓝色是0出栈时计算的雨水储量,出栈后我们把蓝色凹槽填平,然后再进行下一次待压入元素与栈顶元素的比较;绿色时1出栈时计算的雨水储量,出栈后我们把绿色凹槽填平,然后再进行下一次待压入元素与栈顶元素的比较。
下面是单调栈解法的代码示例:
#include <iostream>
#include <vector>
using namespace std;
int trap(vector<int>& height){
stack<int> s;
int sum = 0;
int n = height.size();
int h, w;
for(int i = 0; i < n; ++i){
while(!s.empty() && height[i] > height[s.top()]){
//形成了凹槽,h[i]就是凹槽的右侧柱子
int m = s.top();
s.pop();
//新的栈顶是凹槽左边的柱子
if(!s.empty()){//保证左边柱子存在
h = min(height[i], height[s.top()]) - height[m];//水量的高
w = i - s.top() - 1;//水量的宽
sum += h * w;
}
}
s.push(i);//把第i个柱子入栈
}
return sum;
}
int main(){
int n, x;
vector<int> h;
cin >> n;
for(int i = 0; i < n; ++i){
cin >> x;
h.push_back(x);
}
int ans = trap(h);
cout << ans << endl;
return 0;
}