秋季PAT的第一题就是单调栈,之前没怎么学过, 因此这次专门学习做下笔记。
单调栈Monotone Stack
概念:
从行为上看,它不仅仅是用存储与访问受限的栈,而是一种辅助工具 ,用于检测数据的单调性变化并作出反应 (表现: 当入栈元素会影响栈总体单调性时,要出栈一些元素以维持单调性)
场景:
向左or向右找到第一个稍大(小)的元素、其索引下标;
确定某条件(单调)下的最长区间 ;
确定区间 构成的极值 ,如max f(la,b]);
Point:
单调递增栈: 指栈内元素的出栈序列 递增 (或递减),而栈内元素 是递减(或递增),即当元素比栈顶小的时候入栈。因此需要输出下一个最大的元素。
当然也有人是直接根据栈内元素大小关系来区别, 比如栈内元素递减就叫做递减栈。
此外没有双向栈的存在。
==>2021年10月26日——明确下定义:单调递减栈为出从栈底往栈顶看,元素大小成递减排列。其实上,正确的理解应该从扫描的角度来看,比如从左往右扫描时,如果元素依次递减,则统统会入栈,e.g. [4,2,1,5,8], ==> 当5入栈时,单调递减的栈[4,2,1]横过来看就是原列表单调递减的子序列。
归纳:
当从左往右扫描维护单调递减栈时,可以求元素左边第一个比他大的元素,也可以求右边第一个比它大的元素
左边第一个比他大的元素:当以入栈元素的角度看时,如果栈内元素一直比它小,则不断pop,直到pop不出去元素,那么剩下的栈顶元素就是左边第一个比它大的元素
右边第一个比它大的元素:当以栈内元素的角度看时,把他pop出去的入栈元素就是当前栈顶元素右边第一个比它大的元素
可见:从不同的角度看,能得到不同的效果,代码编写的区别在于ans数组的更新:一个在while不断pop的过程中更新(栈内被弹走元素的角度),另一个是while到pop停下后更新(入栈元素的角度)。
实际上,左边第一个大的元素和右边第一个大的元素是等价的,因为当从左往右扫描时求右边第一个大的元素问题,当吧原序列翻转(或是从右往左扫描时),得到的结果就是对原序列而言每个数左边第一个大的元素。
核心思想
在元素Y入栈的时候会跟栈顶元素X比较 , 如果Y比栈内所有元素Xs都大的话,就可以拿到栈里所有的元素即区间。关键是这个比较, 就可以找到第一个满足要求的数据。
e.g.有列表[1, 3, 2, 0, 7],从左向右遍历,当遍历为7时栈里有[3, 2, 0],此时入栈元素为7, 能得到==>那么对于0来说,右边最大的就是当前入栈元素7, 左边最小的就是栈内下一个元素2。而对3来说,此时3上边的元素就都是比3小的元素们。
Code模板:
1 2 3 4 5 6 7 8 9 def getFirstMax (nums: List[int]) : stack = [] for i in range(len(nums) - 1 , -1 , -1 ): val = nums[i] if stack and val > stack[-1 ]: stack.pop() stack.append(val)
这种写法, 主要是运用的stack.pop元素
例题
Leetcode#496:
单调栈
1 2 3 4 5 6 7 8 9 10 class Solution : def nextGreaterElement (self, nums1: List[int], nums2: List[int]) -> List[int]: hash_dict = dict() stack = [] for i in nums2: while stack and i > stack[-1 ]: hash_dict[stack.pop()] = i stack.append(i) return [hash_dict.get(i,-1 ) for i in nums1]
单调栈
Q: 为什么想到了单调栈?
A:参看题解:首先把A数组中的以A[0]开头的递减序列抽取出来,我们最后要求的最大的宽度坡一定是以这个序列中的某一个i为坡底的,我们反证一下
假设存在某个元素位置k不存在于上面的递减序列中,且有最大宽度j-k,这也就说明k位置的元素一定是小于k前面所有的元素的,否则就会有更长的宽度,但是既然k小于前面所有的元素,那么k就一定会被加入到序列中,与假设矛盾,所以不存在k,解一定存在递减序列中
这样的话我们可以逆向遍历数组,每次遇到元素大于栈顶的就可以计算宽度,然后将栈顶弹出,因为是逆序遍历的,所以这个宽度一定是栈顶这个坡底i能形成的最大宽度了, 逆序遍历再往前的话即使大于这个栈顶,形成的宽度也只会减小,所以这个栈顶是可以直接pop出去的,我们遍历所有的坡底求最大值就行了,时间复杂度O(N)
作者:resolmi https://leetcode-cn.com/problems/maximum-width-ramp/solution/java-dan-diao-zhan-er-fen-jie-fa-chang-shi-jie-shi/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <bits/stdc++.h> using namespace std ;int maxWidthRamp (vector <int >& A) { stack < int > st; int len = A.size(); for (int i = 0 ; i < len; i++) { if (st.empty() || A[i] <= A[st.top()]) st.push(i); } int ans = 0 ; int i = len - 1 ; while ( i > ans){ while ( !st.empty() && A[st.top()] <= A[i] ){ ans = max(ans, i - st.top()); st.pop(); } i --; } return ans; } int main () { vector <int > A = {6 ,0 ,8 ,2 ,1 ,5 }; int ans = maxWidthRamp(A); cout <<ans <<endl ; return 0 ; }
Leetcode#1124:
前缀和+单调栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <bits/stdc++.h> using namespace std ;int longestWPI (vector <int >& hours) { int len = hours.size(); for (int i = 0 ; i < len; i ++ ){ if ( hours[i] > 8 ) hours[i] = 1 ; else hours[i] = -1 ; } vector <int > sum(len + 1 , 0 ); for (int i = 1 ; i <= len; i++ ){ sum[i] = sum[i - 1 ] + hours[i - 1 ]; } stack <int > st; for (int i = 0 ; i <= len; i++ ){ if ( st.empty() || sum[i] < sum[st.top()]) st.push(i); } int ans = 0 ; for (int i = len; i >= 0 ; i--) { while ( !st.empty() && sum[st.top()] < sum[i]){ ans = max(ans, i - st.top()); st.pop(); } } int i = len; while ( i > ans){ while ( !st.empty() && sum[st.top()] < sum[i]){ ans = max(ans, i - st.top()); st.pop(); } i -= 1 ; } return ans; } int main () { vector <int > A = {9 ,9 ,6 ,0 ,6 ,6 ,9 }; int ans = longestWPI(A); cout <<ans <<endl ; return 0 ; }
参看:Bilibili【带写03】python前缀和与单调栈.mp4
单调队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <bits/stdc++.h> using namespace std ;struct node { int val; int id; }; vector <int > v1; vector <int > v2; int n, k;deque <node> q1;deque <node> q2;int main () { cin >> n >> k; int val; for (int i = 1 ; i <= n; i++) { node nd; cin >> nd.val; nd.id = i; while ( !q1.empty() && nd.val <= q1.back().val){ q1.pop_back(); } while ( !q2.empty() && nd.val >= q2.back().val) q2.pop_back(); q1.push_back(nd); q2.push_back(nd); if ( i - q1.front().id + 1 > k) q1.pop_front(); if ( i - q2.front().id + 1 > k) q2.pop_front(); if (i >= k){ v1.push_back(q1.front().val); v2.push_back(q2.front().val); } } int len = v1.size(); for (int i = 0 ; i < len; i++) { cout << v1[i] << " " ; } cout << endl ; len = v2.size(); for (int i = 0 ; i < len; i++) { cout << v2[i] << " " ; } return 0 ; }
holiday
题目描述
经过几个月辛勤的工作,FJ决定让奶牛放假。
假期可以在1…N天内任意选择一段(需要连续),每一天都有一个享受指数W。但是奶牛的要求非常苛刻,假期不能短于P天,否则奶牛不能得到足够的休息;假期也不能超过Q天,否则奶牛会玩的腻烦。
FJ想知道奶牛们能获得的最大享受指数。
输入格式
第一行:N,P,Q.
第二行:N个数字,中间用一个空格隔开。
输出格式
一个整数,奶牛们能获得的最大享受指数。
样例数据
input
output
Hint 选择第3-4天,享受指数为-3+8=5。
数据规模与约定
50% 1≤N≤10000,100% 1≤N≤100000
时间限制:1s, 空间限制:256MB
思路:
用前缀和处理前i天的指数和
其实就是从P的位置开始枚举,每次把i-P压入队列,如果i-Q大于队首元素的位置 ,弹出队首
每次取出队首让ans=max(sum[i]-sum[Day.front()])
最后输出ans
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <bits/stdc++.h> using namespace std ;#define INF 9999999999 long long sum[100300 ];int n,p,q;long long ans=-INF;deque <long long > Day; int main () { freopen("holiday.in" ,"r" ,stdin ); scanf ("%d%d%d" ,&n,&p,&q); long long x; for (int i=1 ;i<=n;i++){ scanf ("%lld" ,&x); sum[i]=sum[i-1 ]+x; } for (int i=p;i<=n;i++){ int nowIndex = i - p; while (!Day.empty() && sum[nowIndex] < sum[Day.back()] ) Day.pop_back(); Day.push_back(nowIndex); while (!Day.empty() && i - q > Day.front()) Day.pop_front(); ans=max(ans, sum[i]-sum[Day.front()]); } cout <<ans; return 0 ; }
总结: 单调队列 对于处理线性滑动区间最值 可谓游刃有余
借鉴: 数据结构之单调队列与单调栈
单调栈
根据题目细品: 第N头牛站最前面, 第1头站最后面, 然后如果Hi > Hn, 则能看到。要求累加第i头牛能看到前面牛的头发数, 即可以理解为第i头牛往前看, 找到比它大的Hi或者边界(边界可以看做为Hi=0的)。===> 输入样例从左往右找比当前元素大的第一个元素,单调递减栈–>正常for i = 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <bits/stdc++.h> using namespace std ;const long long INF = 0x3f3f3f3f ;int n;struct cow { int id; int height; }; stack <cow> st;int main () { int tmp; int ans = 0 ; cin >> n; for (int i = 1 ; i <= n; i++) { cin >> tmp; while ( !st.empty() && tmp >= st.top().height ) { ans += ( i - st.top().id - 1 ); st.pop(); } st.push({i, tmp}); } while (!st.empty()){ int now = st.top().id; st.pop(); ans += (n - now); } cout << ans <<endl ; return 0 ; }
思路2:
每次输入一头牛的身高,找比这头牛矮的,出栈
剩下的牛皆可以看到这只牛
ans值加等于栈中牛的个数
这头牛入栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <bits/stdc++.h> using namespace std ;int n,t;long long ans; stack <int > a;int main () { cin >>n; for (int i=1 ; i<=n; i++) { cin >>t; while (!a.empty() && t >= a.top() ) a.pop(); ans+=a.size(); a.push(t); } cout <<ans; return 0 ; }
从左往右找比当前元素大的–>如果当前元素比栈顶元素大,那么栈顶元素右边比其第一个大的元素就是当前入栈元素—>单调递减栈(从栈底到栈顶递减)–>val > st.top()
,正常for i = 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <bits/stdc++.h> using namespace std ;const int maxn = 1e3 ;int n;int ans[maxn]; struct temperature { int id; int tmp; }; int main () { stack <temperature> st; cin >> n; int val; for (int i = 0 ; i < n; i++) { cin >> val ; while ( !st.empty() && val > st.top().tmp ){ ans[st.top().id] = i - st.top().id ; st.pop(); } st.push({i, val}); } for (int i = 0 ; i < n; i++) { cout <<ans[i] << " " ; } cout << endl ; return 0 ; }
逆序写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <bits/stdc++.h> using namespace std ;const int maxn = 1e3 ;int n;int ans[maxn]; struct temperature { int id; int tmp; temperature(){} temperature(int _id, int _tmp): id(_id), tmp(_tmp){} }; int main () { stack <temperature> st; cin >> n; int val; vector <int > v(n+1 ); for (int i = 1 ; i <= n; i++) { cin >> v[i]; } for (int i = n; i >= 1 ; i--) { int val = v[i]; while ( !st.empty() && val >= st.top().tmp) st.pop(); ans[i] = st.empty()? 0 : st.top().id - i; st.push(temperature(i, val)); } for (int i = 0 ; i < n; i++) { cout <<ans[i] << " " ; } cout << endl ; return 0 ; }
模板写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <bits/stdc++.h> using namespace std ;const int maxn = 1e3 ;int n;int main () { scanf ("%d" , &n); vector <int > v; v.resize(n+1 ); stack < pair<int , int > > st; vector <int > ans(n+1 ); for (int i = 1 ; i <= n; i++) { scanf ("%d" , &v[i]); while ( !st.empty() && v[i] > v[st.top().first]){ ans[ st.top().first ] = i; st.pop(); } st.push({i, v[i]}); } for (int i = 1 ; i <= n; i++) { if ( i == 1 ) printf ("%d" , ans[i]); else printf (" %d" , ans[i]); } return 0 ; }
for遍历倒着写
1 2 3 4 5 6 7 8 for (int i = n; i >= 1 ; i--) { while ( !st.empty() && v[i] >= v[st.top().first]){ st.pop(); } ans[ i ] = st.empty() ? 0 : st.top().first; st.push({i, v[i]}); }
// 找的题解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <bits/stdc++.h> using namespace std ;int n,a[3000001 ],b[3000001 ];stack <int > q;int main () { cin >>n; for (int i=1 ;i<=n;i++) cin >>a[i]; for (int i=n;i>=1 ;i--){ while (!q.empty() and a[q.top()]<=a[i]) q.pop(); if (q.empty()) b[i]=0 ; else b[i]=q.top(); q.push(i); } for (int i=1 ;i<=n;i++) cout <<b[i]<<" " ; return 0 ; }
区别在于: 站内元素是栈顶元素的右边; 而顺着写, 那么栈内元素是栈顶元素的左边, 即栈顶元素为站内元素的右边;同时还有一个区别是: 顺着先能边读边写, 而倒着写必须读完再写
不用v[]的写法, 其实直接可以用stack< pair<int, int> >st
来写,可能会更直观一些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <bits/stdc++.h> using namespace std ;const int maxn = 1e3 ;int n;int main () { int val; scanf ("%d" , &n); stack < pair<int , int > > st; vector <int > ans(n+1 ); for (int i = 1 ; i <= n; i++) { scanf ("%d" , &val); while ( !st.empty() && val > st.top().second ){ ans[ st.top().first ] = i; st.pop(); } st.push({i, val}); } for (int i = 1 ; i <= n; i++) { if ( i == 1 ) printf ("%d" , ans[i]); else printf (" %d" , ans[i]); } return 0 ; }
总结
单调栈可利用的点:
剩下的元素 跟 当前元素的关系;弹出元素的个数;剩余元素的个数
栈顶 和 当前元素的关系==> 找到第一个比数大、小的
递减栈的while中条件写 val > st.top()的时候pop,正着写和反着写都一样, 但是要注意等于号的区别;顺着写判断条件完全满足题意(利用pop的动作做处理),而逆着则要考虑先后,如气温题,找第一个反而要加等号, 要pop掉直到左边第一个出现。
===
一般情况下,都是写while()里只有pop的,nums[i] < nums[st.peek()]
==>st.pop();
,则是维护了一个单调递增栈,对于剩下的栈顶元素而言,入栈元素是>=栈顶元素的,并且对于入栈元素而言找到的是左边第一个大于他的数。单调递减栈,则求的是左边第一个比他小的数。 ===>
如果要求右边的,法一:逆序构建单调栈;法二:从栈顶元素的视角来看(赋值在while里面),只不过这种做法最终栈里会有剩余元素,因此需要对记录数组 赋初值。如:2055. 蜡烛之间的盘子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 class Solution { public int [] platesBetweenCandles(String s, int [][] queries) { int len = s.length(); int [] arr = new int [len]; for (int i = 1 ; i < len; i++) { char c = s.charAt(i - 1 ); if (c == '*' ) { arr[i] = arr[i - 1 ] + 1 ; } else { arr[i] = arr[i - 1 ]; } } Stack<Integer> minSt = new Stack<>(); Stack<Integer> maxSt = new Stack<>(); int [] minRight = new int [len], maxLeft = new int [len]; Arrays.fill(minRight, len); Arrays.fill(maxLeft, -1 ); for (int i = 0 ; i < len; i++) { char c = s.charAt(i); if ( c == '|' ){ minRight[i] = i; maxLeft[i] = i; } while (!minSt.empty() && c == '|' && s.charAt(minSt.peek()) == '*' ) { minRight[minSt.peek()] = i; minSt.pop(); } minSt.push(i); char ch = s.charAt(len - i - 1 ); while (!maxSt.empty() && ch == '|' && s.charAt(maxSt.peek()) == '*' ) { maxLeft[maxSt.peek()] = len - i - 1 ; maxSt.pop(); } maxSt.push(len - i - 1 ); } int [] ans = new int [queries.length]; for (int i = 0 ; i < queries.length; i++) { int l = queries[i][0 ], r = queries[i][1 ]; int ll = minRight[l], rr = maxLeft[r]; if (ll < rr) { ans[i] = arr[rr] - arr[ll]; }else { ans[i] = 0 ; } } return ans; } }