秋季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;     } }