======================= **基础知识** =======================
单调队列(monotone-Queue):重点关注队首元素
目的: 主要为了解决RMQ(range Minimum/Maximum Query) 区间最值问题;
如果不移除开头元素,则最终deque 中留下的元素,则是用来解决固定末尾RMQ问题;
本质来讲,由每次往后移位后对应的deque , 就可以得到以当前值为结尾,任意区间范围内的最值;
单调序列是对原序列中的另外一种信息的表现形式;
数据结构性质: deque(double end queue) 存储数据,用来维护滑动窗口(逻辑概念)内数据的单调性(单调递增/减),从而保证队首元素为当前区间最值;
注意:队尾元素并没有任何特性;
单调队列的相对位置,与原序列相同;
具体操作: 窗口移动过程中,如果新加入的值破坏单调性,对deque 进行 pop_back 直到新加入元素可以插入尾部,并满足单调性;
如果头部最值被移出窗口范围,pop_front 弹出数据;
入队操作: 队尾入队,把之前破坏单调性的元素从队尾移出(维护单调性);
出队操作: 如果队首元素超出区间范围,将元素从队首出队(维护生命周期);
单调栈(monotome-Stack):重点关注每个元素存储过程中,相互关系;
目的:擅长维护最近【大于/小于】关系,实现检索到前面&后面中最近(第一个)大于/小于 他的元素;
下面有个比方:
所有被我打动了的,我是她们的男神;--> 我是她们后面第一个比她们最大的元素; (基于单调递减栈)
那个我打动不了的,她是我的女神。 --> 她是前面第一个比我大,或者相等的元素;(基于单调递减栈)
数据结构性质:单调递增: 最近小于关系;留下的值意味着后面没有比他更小的值;
单调递减: 最近大于关系; 留下的值意味着后面没有比他更大的值;
左侧先入栈的,是维护左侧最近关系;右侧先入栈的,是维护右侧最近关系;
======================= **代码演示** =======================
单调队列: 模板题 239. 滑动窗口最大值
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 };
单调栈:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
======================= **经典问题** =======================
1. 单调队列处理过程,窗口的灵活运用; 好好体会 窗口是一个逻辑概念;
对于单调队列,窗口可以是固定的大小,也可以是一个满足某种条件下的范围变化;
在单调队列每压入新元素时,队列中存在的都是基于当前元素为结尾且满足单调性的元素,并且按照下标顺序;
每次都有可能完全更改队列元素,但是性质一定是不变的; 而且从数值角度来讲,只要元素能一直保存下来,那后面的元素会离这个保留下来的元素越来越近;
862. 和至少为 K 的最短子数组 : 单调队列中元素的相对位置,与原序列相同;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 };
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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只要一组,求最短;
每次压入新元素过程,就是一次以当前元素为末尾,获得区间最值;这时候也是窗口移动的机会;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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中元素,求和;
单调栈解法: 处理每个元素时候,可以得到以该元素为最小值的连续子数组的前后范围,在该范围内包含该元素的所有连续子数组个数可得,求和;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 的额外条件限制;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 };
3. 单调栈:求解元素的最近的大于/小于关系时候,才使用单调栈;
503. 下一个更大元素 II : 单调栈模板题,体会下被打动的,我都是她们的男神; 单调递减栈,对于被弹出元素, 我是在她们后面第一个大于她的;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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. 股票价格跨度 : 单调栈模板题: 体会下打动不了的,她是我女神; 单调递减栈,对于当前压入元素,前面元素第一个大于等于我的;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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. 柱状图中最大的矩形 : 需要同时记录前后的第一个小于元素;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 模式 : 重点在与如何获取:元素后面小于它的最大值
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 };
6. 42. 接雨水 : 分层求解
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 位数字 : 使用其他数据结构 模拟单调栈的性质;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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. 去除重复字母 : 增加进出条件;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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. 最小栈 : 数据结构本质是定义一种性质的,数据结构封装过程中要维护好性质,利用这一性质来解决问题;
下面定义的数据结构就是为了针对题目的定义的性质,不是单调栈数据结构;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 */
======================= **应用场景** =======================