======================= **基础知识** =======================

单调队列(monotone-Queue):重点关注队首元素

  目的: 主要为了解决RMQ(range Minimum/Maximum Query) 区间最值问题;

     如果不移除开头元素,则最终deque 中留下的元素,则是用来解决固定末尾RMQ问题;

     本质来讲,由每次往后移位后对应的deque , 就可以得到以当前值为结尾,任意区间范围内的最值;

     单调序列是对原序列中的另外一种信息的表现形式;

  数据结构性质: deque(double end queue) 存储数据,用来维护滑动窗口(逻辑概念)内数据的单调性(单调递增/减),从而保证队首元素为当前区间最值

       注意:队尾元素并没有任何特性;

       单调队列的相对位置,与原序列相同

  具体操作: 窗口移动过程中,如果新加入的值破坏单调性,对deque 进行 pop_back 直到新加入元素可以插入尾部,并满足单调性;

        如果头部最值被移出窗口范围,pop_front 弹出数据;

        入队操作: 队尾入队,把之前破坏单调性的元素从队尾移出(维护单调性);

        出队操作: 如果队首元素超出区间范围,将元素从队首出队(维护生命周期);

 

单调栈(monotome-Stack):重点关注每个元素存储过程中,相互关系;

  目的:擅长维护最近【大于/小于】关系,实现检索到前面&后面中最近(第一个)大于/小于 他的元素;

    下面有个比方:

    所有被我打动了的,我是她们的男神;--> 我是她们后面第一个比她们最大的元素; (基于单调递减栈)

    那个我打动不了的,她是我的女神。   --> 她是前面第一个比我大,或者相等的元素;(基于单调递减栈)

  数据结构性质:单调递增: 最近小于关系;留下的值意味着后面没有比他更小的值;

           单调递减: 最近大于关系; 留下的值意味着后面没有比他更大的值;

    左侧先入栈的,是维护左侧最近关系;右侧先入栈的,是维护右侧最近关系;

          
======================= **代码演示** =======================

单调队列: 模板题 239. 滑动窗口最大值 

 1 class Solution {
 2 public:
 3     vector<int> maxSlidingWindow(vector<int>& nums, int k) {
 4         vector<int> ans;
 5         deque<int> deq;
 6         for(int i = 0, I = nums.size(); i < I; i++) {
 7             while(deq.size() && nums[deq.back()] < nums[i]) deq.pop_back();
 8             deq.push_back(i);
 9             if(deq.front() <= (i - k)) deq.pop_front();
10             if((i + 1) < k) continue;
11             ans.push_back(nums[deq.front()]);
12         }
13         return ans;
14     }
15 };
单调队列模板题

单调栈:

 1 //input : 10 6 7 9 0 8 3 4 5 1 2 
 2 
 3 
 4 //#include "monotoneStack.h"
 5 #include <iostream>
 6 #include <vector>
 7 #include <stack>
 8 
 9 using namespace std;
10 
11 void output(const char *msg, vector<int>& arr) {
12     printf("%s", msg);
13     for(auto x : arr)  printf("%5d", x);
14     printf("\n");
15     return;
16 }
17 
18 int main()
19 {
20     int n;
21     cin >> n;
22 
23     vector<int> ind(n);
24     vector<int> arr(n);
25     stack<int> s;   //单调递增栈
26     vector<int> pre(n), next(n);  //某个值的前面离他最近比他小的值(pre),后面离他最近比他小值(next)
27 
28     for(int i = 0; i < n; i++) cin >> arr[i], ind[i] = i;
29     for(int i = 0; i < n; i++) {
30         while(s.size() && arr[i] < arr[s.top()]) {
31             next[s.top()] = i;
32             s.pop();
33         }
34         if(s.size() == 0) pre[i] = -1;
35         else pre[i] = s.top();
36         s.push(i);
37     }
38 
39     while(s.size()) next[s.top()] = n, s.pop();
40     
41     output("ind:", ind);
42     output("arr:", arr);
43     output("pre:", pre);
44     output("nxt:", next);
45 
46     return 0;
47 }
MonotoneStack

======================= **经典问题** =======================

1. 单调队列处理过程,窗口的灵活运用; 好好体会 窗口是一个逻辑概念

  对于单调队列,窗口可以是固定的大小,也可以是一个满足某种条件下的范围变化;

  在单调队列每压入新元素时,队列中存在的都是基于当前元素为结尾且满足单调性的元素,并且按照下标顺序;

  每次都有可能完全更改队列元素,但是性质一定是不变的; 而且从数值角度来讲,只要元素能一直保存下来,那后面的元素会离这个保留下来的元素越来越近;

862. 和至少为 K 的最短子数组  : 单调队列中元素的相对位置,与原序列相同;

 1 //original
 2 class Solution {
 3 public:
 4     int shortestSubarray(vector<int>& nums, int k) {
 5         int ans = nums.size() + 1;
 6         vector<long long> preSum(ans, 0);
 7         deque<int> SumInc(1, 0); 
 8 
 9         for(int i = 0, I = nums.size(); i < I; i++) {
10             preSum[i + 1] = preSum[i] + nums[i];
11 
12             //在单调递增队列中,每轮中处理后的队列中的元素相对位置,与原序列相同;
13             //对于窗口的调整,依据每轮中最大值与 front 值 > k,则可以不断尝试缩小左边窗口;
14             //对于后面进入的元素,要想距离更短,必然在上一轮满足>k 条件 front 点的后面;
15             while(SumInc.size() && preSum[SumInc.back()] >= preSum[i + 1]) SumInc.pop_back();
16             SumInc.push_back(i + 1);
17             int temp = -1;
18             while(preSum[i + 1] - preSum[SumInc.front()] >= k) {
19                 temp = SumInc.front();
20                 SumInc.pop_front();
21             }
22             if(temp == -1) continue;
23             if (ans > (i + 1 - temp)) ans = i + 1- temp;
24         }
25         if(ans > nums.size()) ans = -1;
26         return ans;
27     }
28 };
抽象窗口的灵活体现

剑指 Offer 59 - II. 队列的最大值

 1 class MaxQueue {
 2 public:
 3     vector<int> arr;
 4     deque<int> dq;
 5     int start;
 6     MaxQueue():start(-1) {}
 7     
 8     int max_value() {
 9         if(arr.size() <= start + 1) return -1;
10         return arr[dq.front()];
11     }
12     
13     void push_back(int value) {
14         while(dq.size() && value >= arr[dq.back()]) dq.pop_back();
15         dq.push_back(arr.size());
16         arr.push_back(value);
17         return;
18     }
19     
20     int pop_front() {
21         if(arr.size() <= start + 1) return -1;
22         start += 1;
23         if(dq.front() == start) dq.pop_front();
24         return arr[start];
25     }
26 };
27 
28 /**
29  * Your MaxQueue object will be instantiated and called as such:
30  * MaxQueue* obj = new MaxQueue();
31  * int param_1 = obj->max_value();
32  * obj->push_back(value);
33  * int param_3 = obj->pop_front();
34  */
35 
36 //origianl 
37 class MaxQueue {
38 public:
39     deque<int> a, ma;
40 
41     MaxQueue():a(deque<int>{}), ma(deque<int>{}) {}
42     
43     int max_value() {
44         if(a.empty() ) return -1;
45         return ma.front();
46     }
47     
48     void push_back(int value) {
49         a.push_back(value);
50         while(ma.size() && ma.last() < value) ma.pop_back();
51         ma.push_back(value);
52         return;
53     }
54     
55     int pop_front() {
56         if(a.empty()) return -1;
57         int ret = a.front();
58         if(ret == ma.front()) ma.pop_front();   //这里的判断front 出队是要想好的
59         a.pop_front();
60         return ret;
61     }
62 };
63 
64 /**
65  * Your MaxQueue object will be instantiated and called as such:
66  * MaxQueue* obj = new MaxQueue();
67  * int param_1 = obj->max_value();
68  * obj->push_back(value);
69  * int param_3 = obj->pop_front();
70  */
灵活的窗口定义

 1438. 绝对差不超过限制的最长连续子数组  : 与上面 862 题做对比,最短用一个优先队列就可以解决,最长必须要两个;

优先队列会不断的用后面的更紧凑的元素来取代前面元素,并保持与原始序列 相同的 相对位置;所以留下的是距离更近的元素,但是距离都会是靠后位置元素;所以 862只要一组,求最短;

每次压入新元素过程,就是一次以当前元素为末尾,获得区间最值;这时候也是窗口移动的机会;

 1 class Solution {
 2 public:
 3     int longestSubarray(vector<int>& nums, int limit) {
 4         deque<int> min_arr, max_arr;
 5         int ret = 0, l = -1, flag = 0;
 6         for(int i = 0, I = nums.size(); i < I; ++i) {
 7             while(max_arr.size() && nums[i] > nums[max_arr.back()]) max_arr.pop_back();
 8             while(min_arr.size() && nums[i] < nums[min_arr.back()]) min_arr.pop_back();
 9             max_arr.push_back(i);
10             min_arr.push_back(i);
11             
12             //谁位置更靠前 并且不满足<limit 条件,谁弹出;
13             //然后以这个弹出位置,为前置节点,寻找下一组满足条件区间;
14             while(min_arr.size() && max_arr.size() && nums[max_arr.front()] - nums[min_arr.front()] > limit) {
15                 if(max_arr.front() < min_arr.front()) {
16                     l =  max_arr.front();
17                     max_arr.pop_front();
18                 } else {
19                     l = min_arr.front();
20                     min_arr.pop_front();
21                 }
22             }
23 
24             ret = max(ret, i - l);
25         }
26         return ret;
27     }
28 };
窗口移动过程

 

2. 907. 子数组的最小值之和 : 重点在与转换题意,单调栈 与 单调队列并没有本质区别;

  RMQ解法:以每个元素为结尾时求RMQ, 然后以该元素位置(index)向前的(index + 1)个连续子数组的最小值  对应为RMQ中元素,求和;

  单调栈解法: 处理每个元素时候,可以得到以该元素为最小值的连续子数组的前后范围,在该范围内包含该元素的所有连续子数组个数可得,求和;

 1 class Solution {
 2 public:
 3     int sumSubarrayMins(vector<int>& arr) {
 4 #define MOD_NUM (long long)(1e9 + 7)
 5 //单调队列RMQ
 6         deque<int> dq; //单调递增
 7         vector<long long> sum(arr.size(), 0);
 8         long long ret = 0;
 9         for(int i = 0, I = arr.size(); i < I; ++i) {
10             while(dq.size() && arr[dq.back()] > arr[i]) dq.pop_back();
11             dq.push_back(i);
12             
13 //用空间换时间,记录下前面元素和值,后面计算涉及到前面元素时候,直接用
14             int pre_ind = dq.size() ? dq.back() : -1;
15             if(pre_ind == -1) sum[i] = arr[i] * (i - pre_ind);
16             else sum[i] = sum[pre_ind] + arr[i] * (i - pre_ind);
17             ret += sum[i];
18 //下面增加时间复杂度
19 //            for(int j = dq.size() - 1; j > 0; --j) {
20 //                ret += (arr[dq[j]] * (dq[j] - dq[j - 1]));
21 //            } 
22 //            ret += (arr[dq[0]] * (dq[0] + 1));
23 
24         }
25 
26 //单调栈
27 //        int len = arr.size();
28 //        vector<int> l(len, -1), r(len, len);
29 //        stack<int> s_inc; 
30 //        for(int i = 0; i < len; ++i) {
31 //            while(s_inc.size() && arr[s_inc.top()] > arr[i]) {
32 //                r[s_inc.top()] = i;
33 //                s_inc.pop();
34 //            }
35 //            if(s_inc.size()) l[i] = s_inc.top();
36 //            s_inc.push(i);
37 //        }
38 //        long long ret = 0;
39 //        for(int i = 0; i < len; ++i) {
40 //            int pre = i - l[i] - 1, nxt = r[i] - i - 1;
41 //            ret += (long long)arr[i] * (long long)(pre + nxt + 1 + pre * nxt);
42 //        }
43 //
44         return ret % MOD_NUM;
45     }
46 };
不同题意转化,数据结构使用也不同

 

1499. 满足不等式的最大值 : 增加对于push/pop 的额外条件限制;

 1 class Solution {
 2 public:
 3     int findMaxValueOfEquation(vector<vector<int>>& points, int k) {
 4         deque<vector<int>> dq_dec; 
 5         int ans = INT_MIN;
 6 
 7         for(int i = 0, I = points.size(); i < I; ++i) {
 8             while(dq_dec.size() && (points[i][0] - dq_dec.front()[0] > k)) dq_dec.pop_front();
 9             if(dq_dec.size()) ans = max(ans, points[i][0] + points[i][1] + dq_dec.front()[1] - dq_dec.front()[0]);
10             while(dq_dec.size() && (dq_dec.back()[1] - dq_dec.back()[0] < points[i][1] - points[i][0])) dq_dec.pop_back(); 
11             dq_dec.push_back(points[i]);
12         }
13         return ans;
14     }
15 };
增加push/pop 的条件

  

3. 单调栈:求解元素的最近的大于/小于关系时候,才使用单调栈;

   503. 下一个更大元素 II : 单调栈模板题,体会下被打动的,我都是她们的男神; 单调递减栈,对于被弹出元素, 我是在她们后面第一个大于她的;

 1 class Solution {
 2 public:
 3     vector<int> nextGreaterElements(vector<int>& nums) {
 4         //单调递减
 5         stack<int> sd;
 6         vector<int> ans(nums.size(), -1);
 7         for(int i = 0, I = nums.size(); i < I; ++i) {
 8             while(sd.size() && nums[sd.top()] < nums[i]) {
 9                 ans[sd.top()] = nums[i];
10                 sd.pop();
11             }
12             sd.push(i);
13         }
14 
15         for(int i = 0, I = nums.size(); i < I; ++i) {
16             while(sd.size() && nums[sd.top()] < nums[i]) {
17                 ans[sd.top()] = nums[i];
18                 sd.pop();
19             }
20             sd.push(i);
21         }
22         return ans;
23     }
24 };
单调栈模板题

  901. 股票价格跨度  : 单调栈模板题: 体会下打动不了的,她是我女神; 单调递减栈,对于当前压入元素,前面元素第一个大于等于我的;

 1 class StockSpanner {
 2 public:
 3     stack<int> s_dec ;
 4     vector<int> nums;
 5     StockSpanner() {}
 6     
 7     int next(int price) {
 8         while(s_dec.size() && nums[s_dec.top()] <= price) s_dec.pop();
 9         nums.push_back(price);
10         int ret = nums.size();
11         if(s_dec.size()) ret = nums.size() - 1 - s_dec.top(); 
12         s_dec.push(nums.size() - 1);
13         return ret;
14     }
15 };
16 
17 /**
18  * Your StockSpanner object will be instantiated and called as such:
19  * StockSpanner* obj = new StockSpanner();
20  * int param_1 = obj->next(price);
21  */
单调栈模板题

 

4. 综合利用单调栈性质: 难点在分析题目需求,以及一些题目中对于相等元素的取舍;

  84. 柱状图中最大的矩形  : 需要同时记录前后的第一个小于元素;

 1 class Solution {
 2 public:
 3     int largestRectangleArea(vector<int>& heights) {
 4         vector<int> pre(heights.size(), -1), nxt(heights.size(), heights.size());
 5         stack<int> s_inc;
 6         for(int i = 0, I = heights.size(); i < I; ++i) {
 7             while(s_inc.size() && heights[s_inc.top()] > heights[i]) {
 8                 nxt[s_inc.top()] = i;
 9                 s_inc.pop();
10             }
11             if(s_inc.size()) pre[i] = s_inc.top();
12             s_inc.push(i);
13         }
14         int ret = 0;
15         for(int i = 0, I = heights.size(); i < I; ++i) {
16             ret = max(ret, heights[i] * (nxt[i] - pre[i] - 1));
17         }
18         return ret;
19     }
20 };
综合利用单调栈性质

 

 5. 456. 132 模式   :  重点在与如何获取:元素后面小于它的最大值

 1 class Solution {
 2 public:
 3     bool find132pattern(vector<int>& nums) {
 4         //132 pattern : 对于3 来讲,要求得前面小于它的最小值, 后面小于它的最大值; 然后对比后面值 是否大于 前面值
 5         // 这里希望1 尽可能小,32 尽可能大,并接近
 6         int len = nums.size();
 7         //前面小于它的最小值,没有就为极限值,保证后面没有大于它
 8         vector<int> preMin(len, 1e9);
 9         for(int i = 1; i < len; ++i) preMin[i] = min(preMin[i - 1], nums[i - 1]);
10         
11         //后面比它小的元素中最大值 
12         //倒序求,到达当前元素时后面元素已经计算结束了;  //解题技巧!
13         //如果当前元素后面有大于它的元素,求当前元素 到 离它最近的大于它的元素中间 最接近它的且小于它的元素;
14         //因为如果 n + i 都不满足条件,n 元素 + (n + i) 后的元素 就更不可能满足条件
15         stack<int> s_dec;
16         for(int i = len - 1; i > 0 ; --i) {
17             int close_i_val = nums[i];
18             while(s_dec.size() && nums[i] > s_dec.top()) {
19                 close_i_val = s_dec.top();
20                 s_dec.pop();
21             }
22             if(close_i_val < nums[i] && preMin[i] < close_i_val) return true;
23             s_dec.push(nums[i]);
24         }
25 
26         return false;
27     }
28 };
利用倒序以及单调栈性质获得 n后面小于它的最大元素

 

6.  42. 接雨水 : 分层求解

 1 class Solution {
 2 public:
 3     int trap(vector<int>& height) {
 4         int len = height.size();
 5         stack<int> s_dec;
 6 //        vector<int> pre(len, -1), nxt(len, -1);
 7         int ret = 0;
 8 
 9         for(int i = 0; i < len; ++i) {
10             while(s_dec.size() && height[s_dec.top()] < height[i]) {
11 //                nxt[s_dec.top()] = i;
12                 int cur_h = height[s_dec.top()];
13                 s_dec.pop();
14                 if(s_dec.empty())  continue;
15                 ret += (min(height[i], height[s_dec.top()]) - cur_h) * (i - s_dec.top() - 1);
16             }
17 //            if(s_dec.size()) pre[i] = s_dec.top();
18             s_dec.push(i);
19         }
20 // 将下面直接移动到每个元素压入栈的过程中
21 //        for(int i = 0; i < len; ++i) {
22 //            if(pre[i] == -1 || nxt[i] == -1) continue;
23 //            int min_h = min(height[pre[i]], height[nxt[i]]);
24 //            ret += ((min_h - height[i]) * (i - pre[i]));
25 //            height[i] = min_h;
26 //        }
27         return ret;
28     }
29 };
分层求解

 

7. 402. 移掉 K 位数字 : 使用其他数据结构 模拟单调栈的性质;

 1 class Solution {
 2 public:
 3     string removeKdigits(string num, int k) {
 4         if(num.size() <= k) return "0";
 5         string s_inc;
 6         int idx = 0;
 7         for(; num[idx] && k; ++idx) {
 8             while(s_inc.size() && s_inc.back() > num[idx] && k) {
 9                 s_inc.pop_back();
10                 k--;
11             }
12             s_inc += num[idx];
13         }
14 
15         if(idx == num.size()) while(k--) s_inc.pop_back();
16         else s_inc += num.substr(idx);
17 
18         idx = 0;
19         while('0' == s_inc[idx] && idx < s_inc.size()) idx++;
20         if(idx == s_inc.size()) return "0";
21         return s_inc.substr(idx);
22 
23 
24 
25 //思路ok, 直接用string 模拟单调栈
26 //        if(num.size() <= k) return "0";
27 //        string ans = "";
28 //
29 //        stack<char> s_inc;
30 //        int idx = 0;
31 //        for(; num[idx] && k; ++idx) {
32 //            while(s_inc.size() && s_inc.top() > num[idx] && k) {
33 //                s_inc.pop();
34 //                k--;
35 //            }
36 //            s_inc.push(num[idx]);
37 //        }
38 //
39 //        if(idx == num.size()) {
40 //            while(k--) s_inc.pop();
41 //        } else ans = num.substr(idx);
42 //
43 //        while(s_inc.size()) {
44 //            ans = s_inc.top() + ans;
45 //            s_inc.pop();
46 //        }
47 //        idx = 0;
48 //        while('0' == ans[idx] && idx < ans.size()) idx++;
49 //        if(idx == ans.size()) return "0";
50 //        return ans.substr(idx);
51     }
52 };
使用其他数据结构,遵守单调栈性质

 

8.  316. 去除重复字母 :  增加进出条件;

 1 class Solution {
 2 public:
 3     string removeDuplicateLetters(string s) {
 4         string ret, ans;     //递增栈,结果
 5         unordered_map<char,int> cnt;    //记录数目,如果已经确定位置就为0
 6 
 7         for(auto x : s)  cnt[x]++;
 8         for(auto x : s) {
 9             if(!cnt[x]) continue;
10             while(ret.size() && x <= ret.back()) {
11                 if(cnt[ret.back()] != 1) {
12                     cnt[ret.back()]--;
13                     ret.pop_back();
14                 } else {
15                     for(auto y : ret) cnt[y] = 0;
16                     ans += ret;
17                     ret = "";
18                 }
19             }
20             ret.push_back(x);
21         } 
22         ans += ret;
23         return ans;
24 
25     }
26 };
增加出栈/入栈条件

 

9. 155. 最小栈 : 数据结构本质是定义一种性质的,数据结构封装过程中要维护好性质,利用这一性质来解决问题;

  下面定义的数据结构就是为了针对题目的定义的性质,不是单调栈数据结构;

 1 class MinStack {
 2 public:
 3     /** initialize your data structure here. */
 4     stack<int> s, s_min;
 5     MinStack() {
 6 
 7     }
 8     
 9     void push(int val) {
10         s.push(val);
11         //求最小值,那么要想办法留下最小值,并在下面该pop 时候pop;
12         //这是这个MinStack 要维护的性质;
13         if(s_min.empty() || s_min.top() >= val) s_min.push(val);
14         return;
15     }
16     
17     void pop() {
18         if(s_min.top() == s.top()) s_min.pop();
19         s.pop();
20         return;
21     }
22     
23     int top() {
24         return s.top();
25     }
26     
27     int getMin() {
28         return s_min.top();
29     }
30 };
31 
32 /**
33  * Your MinStack object will be instantiated and called as such:
34  * MinStack* obj = new MinStack();
35  * obj->push(val);
36  * obj->pop();
37  * int param_3 = obj->top();
38  * int param_4 = obj->getMin();
39  */
数据结构的性质是为了解决问题

 

======================= **应用场景** =======================

 

posted on 2022-03-15 22:49  学海一扁舟  阅读(187)  评论(0编辑  收藏  举报