单调栈原理
先说结论,单调栈就是维护了一个数组B,B[i-1]是B[i]的左边最近小/大位置。 且B[i]若被j pop出去,那么j就是B[i]的右边最近小/大的位置。以此得到所有位置左右最近小/最近大位置。
之前说过单调栈的使用场景和应该使用单调增还是减的栈:单调栈应用技巧-CSDN博客
这次分析下为什么单调栈可以解决这类问题。
假如要求长度为n的数组A, 在任意位置的最近小的下标。可以遍历0到n-1,遍历的同时维护一个动态长度数组B。 如果当前遍历到i, 设此时B长度为m,那么对于任意j<m, 都有:B[j-1]是B[j]的左边最近小位置。而且B[m-1]等于i-1。
这样以来,处理i时:
若A[B[m-1]]<A[i]: 那么B[m-1]一定是i最近小的位置(相邻嘛),则直接把i插到B的最后即可。
反之,我们可以得到 i最近小的位置一定<=B[m-1]的最近小的位置(此处可以用反证法),而B[m-1]最近小的位置就是B[m-2], 所以 i最近小的位置一定<=B[m-2],现在再比较A[B[m-2]]和A[i],如果还是A[B[m-2]]>=A[i],就再找B[m-3]...,直到找到A[B[m-k]]<A[i],那么B[m-k]一定是i的最近小。这些操作就等于pop掉B[m-1]到B[m-k+1]。 最后直接把i插到B的最后即可。
以上操作就维护了数组B,保证里面相邻两个数存的都是最近小的编号,换个角度看其实就是单调栈。
leetcode 84为例:
84. 柱状图中最大的矩形 - 力扣(LeetCode)
这里算下标i的贡献时:找到i的左边最近小left_less[i]和右边最近小right_less[i]。直接那么i其实能向左延伸到left_less[i]+1, 向右延伸到right_less[i]-1,贡献值为:((right_less[i]-1)-(left_less[i]+1)+1) * A[i]
代码如下
class Solution {
public:int largestRectangleArea(vector<int>& A) {stack<int> B;int n = A.size();vector<int> left_less(n);for(int i=0; i<n; i++){left_less[i] = i;while(!B.empty()&&A[B.top()]>=A[i]){B.pop();}if(!B.empty()){left_less[i] = B.top();}else{left_less[i] = -1;}B.push(i);}while(!B.empty()){B.pop();}vector<int> right_less(n);for(int i=n-1; i>=0; i--){right_less[i] = i;while(!B.empty()&&A[B.top()]>=A[i]){B.pop();}if(!B.empty()){right_less[i] = B.top();}else{right_less[i] = n;}B.push(i);}int ret= 0;for(int i=0; i<n; i++){ret = max(ret, ((right_less[i]-1)-(left_less[i]+1)+1)*A[i]);}return ret;}
};
更进一步思考,其实从左向右遍历时,假设i被j pop出去的,那么j一定是i右边的最近小(很好证明),这样就不用单独逆序遍历求right_less[i]了,直接在i被pop时记录就行了。
上代码:
class Solution {
public:int largestRectangleArea(vector<int>& A) {A.push_back(-1);stack<int> B;int n = A.size();vector<int> left_less(n);vector<int> right_less(n);for(int i=0; i<n; i++){left_less[i] = i;right_less[i] = i;while(!B.empty()&&A[B.top()]>=A[i]){right_less[B.top()] = i;B.pop();}if(!B.empty()){left_less[i] = B.top();}else{left_less[i] = -1;}B.push(i);}int ret= 0;for(int i=0; i<n; i++){ret = max(ret, ((right_less[i]-1)-(left_less[i]+1)+1)*A[i]);}return ret;}
};