当前位置: 首页 > news >正文

单调栈单调队列【算法进阶】

这周学完之后最大的收获就是单调栈和单调队列了!!!感觉好厉害能把时间复杂度瞬间压缩为O(N),不行我必须再纪念一下这么美妙的算法!!!

单调栈问题:

如果题目要求一个元素左边或右边的第一个大于或小于他的元素(一把不会直接告诉你,需要你在分析问题时对是否有单调性或者是否需要单调性进行分析),或者让你求一段连续子数组的左右边界问题(如柱状图的最大矩形面积)时(力扣上问题84、85、或者洛谷上最大区间问题等等),可以优先考虑用单调栈,而如果要以当前元素为右端点,在他之前找到极值时,一般用滑动窗口,而且滑动窗口一般不会只考双端队列,而是用单调队列进行优化(队首/或者说越靠前一定是最优选择)例如力扣862.和至少为K的子数组,先用前缀和预处理,要想用最短的长度满足大于等于K 那么就要让窗口尽量小,且遍历时右端元素与队首元素的差最大,也就是让队首元素最小,固需要维护单调递增队列,用于找到目前以这个元素为右端点时前面的最小值!

进阶题目练习:

1、柱状图中的最大矩形

这个题目就是让求一段连续的区间,然后让区间长度与区间内的所有元素的最小值的乘积最大,首先用暴力的方法来想,那就是遍历每一个矩形,那么怎么优化呢,我们可以遍历每一个元素,然后以当前元素为最小值的连续矩形,这样我们就从遍历每一个矩形变成了遍历每一个可能作为答案的矩形(最多有n个矩形),那么怎么去实现找到以当前元素为最小值的矩形呢?开个循环往两边找边界?诶?往两边找?边界?那不就是分别找当前元素两边的第一个小于他的位置吗?恭喜你,想到了正解 -> 单调递增栈优化,通过正反两次单调递增栈找到以这个元素为最小值的矩形的左右边界即可。        【从暴力优化到正解!✌️】

class Solution {
public:int largestRectangleArea(vector<int>& heights) {int ans = 0,n = heights.size();vector<int> l(n),r(n);stack<int> st;for(int i=n-1;i>=0;i--){int t = heights[i];while(!st.empty() && heights[st.top()] >= t) st.pop();r[i] = (st.empty() ? n : st.top());st.push(i);}st = stack<int> ();for(int i=0;i<n;i++){int t = heights[i];while(!st.empty() && heights[st.top()] >= t) st.pop();l[i] = (st.empty() ? -1 : st.top());st.push(i);}for(int i=0;i<n;i++){ans = max(ans,(r[i] - l[i] - 1)*heights[i]);}return ans;}
};

至于最大区间,和上面的题一模一样,就当复习了,代码略~

什么?难度又要升级了?来看一下:最大矩形

还是一样,先想暴力,枚举每一个矩形,找出最大值,从上往下来看,这不是和前面的题目一样吗,从一个个柱状图中找出最大的连续矩形(可是有可能同一列的柱状图不连续啊)那就只能一行一行的找喽,那就对每一行进行上一题的步骤,不断更新最大值,总比纯暴力枚举每一个矩形强吧,时间复杂度是O(N*M),看一眼数据范围能过。

怎么实现呢?首先进行预处理,可以先把每一行的连续柱状图表示出来,然后对每一行进行两次单调栈查询以当前元素为最小值的矩阵的左右范围,不断更新即可!

class Solution {
public:int maximalRectangle(vector<vector<char>>& matrix) {int n = matrix.size();//n行int m = matrix[0].size();//m列vector<vector<int>> dp(n,vector<int>(m,0));//预处理值(这行中这一列的连续的1的个数即:高度)for(int j=0;j<m;j++)for(int i=0;i<n;i++)if(matrix[i][j] == '1')dp[i][j] = (i == 0) ? 1 : dp[i-1][j] + 1;int ans=0;for(int i=0;i<n;i++)//对每一行都仿照84题的解法{vector<int> l(m,0),r(m,0);//以当前元素为最小值的左右区间stack<int> st;for(int j=0;j<m;j++){int t = dp[i][j];while(!st.empty() && dp[i][st.top()] >= t) st.pop();l[j] = st.empty() ? -1 : st.top();st.push(j);}st = stack<int> ();for(int j = m-1;j>=0;j--){int t = dp[i][j];while(!st.empty() && dp[i][st.top()] >= t) st.pop();r[j] = st.empty() ? m : st.top();st.push(j);}for(int j=0;j<m;j++){int x = r[j] - l[j] - 1;int S = x*dp[i][j];ans = max(ans,S);}}return ans;}
};

太美妙了这单调栈!

滑动窗口&单调队列优化

双端队列滑动窗口(常规滑动窗口)

单调队列滑动窗口
                                               1.维护左端点

                                               2.维护右端点的队列单调性

                                               3.元素入队 

长度最小的子数组

由于每一个元素都大于0,所以前缀和本身就具有单调性,无需用到单调队列进行优化,直接二分前缀和即可,或者用双端队列/双指针模拟滑动窗口来实现。

class Solution {
public:int minSubArrayLen(int target, vector<int>& nums) {int ans=1e8;deque<int> q;int sum=0;for(int i=0;i<nums.size();i++){q.push_back(i);//本身就具有单调性 所以可以直接进队sum+=nums[i];while(!q.empty() && sum>=target){ans = min(ans,q.back() - q.front() + 1);sum-=nums[q.front()];q.pop_front();}  }return ans==1e8 ? 0 : ans;}
};

当然更推荐符合大众的模板风格:维护左端,维护结束后再判断答案

这样写的好处是只要遍历到了一个合法答案 那么之后的所有遍历中答案都合法了,所以再更新答案时要加上if判断合法答案,防止一次while都没进过 就应该是没有合法答案了 所以while中是多余元素的判断!

class Solution {
public:int minSubArrayLen(int target, vector<int>& nums) {int ans=1e8;deque<int> q;int sum=0;for(int i=0;i<nums.size();i++){q.push_back(i);//本身就具有单调性 所以可以直接进队sum+=nums[i];while(!q.empty() && sum - nums[q.front()]>=target){sum-=nums[q.front()];q.pop_front();} //if是必须要加的防止一次while都没进入过!!!if(sum >= target) ans = min(ans,q.back() - q.front() + 1);}return ans==1e8 ? 0 : ans;}
};

和至少为K的最短子数组

这道题让求满足一定条件的子数组,而且还要求出最短的长度,首先想暴力怎么实现,遍历每一个子数组,再计算区间和(前缀和优化),然后再找出最小的满足条件的子数组,显然时间复杂度是通过不了此题的,那么就开始想怎么去优化一下,首先毋庸置疑要先用前缀和进行预处理,然后再找满足条件的子区间 -> 要想使区间和大于等于K,那么就要在遍历数组时,让区间的左端点尽量的小,因为这样才能保证区间尽可能的大(首先要保证存在性,然后再取最优解),而且还要保证区间的左端点一定要比右端点小,也就是说在遍历时,如果当前元素是目前为止出现的最小值了,那么就应该将这个元素为起点继续往后找了因为越小的值越作为起点,继续往后遍历时肯定是要找他之前的最小值),这样也就符合单调队列的性质了,所以单调队列优化滑动窗口可以适用于求一个元素前面的最值问题,基本步骤是先维护左端点(并计算答案【有存在解时】)再通过右端点维护队列的单调性,然后最后再从队尾push_back新元素。所以就有一下代码:

class Solution {
public:int shortestSubarray(vector<int>& nums, int k) {int n = nums.size();vector<long long> sum(n+1,0);//前缀和预处理for(int i=0;i<nums.size();i++) sum[i+1] = sum[i] + nums[i];int ans = 1e8;deque<int> q;q.push_back(0);//因为是前缀和 所以要多往前压一个下标for(int i=1;i<=n;i++){int t = sum[i];//在满足条件情况下更新ans并将队首弹出while(!q.empty() && sum[i] - sum[q.front()] >= k){//因为往前移了一个元素 所以无需-1ans = min(ans,i-q.front());q.pop_front();}//维护单调递增队列while(!q.empty() && sum[q.back()] >= t) q.pop_back();q.push_back(i);}return ans==1e8 ? -1 : ans;}
};

最小覆盖子串 

附:先加入新元素和后加入新元素的判断:

先加入新元素和后加入区别:

先加入是因为while中是合法解,要在while中找答案,while之后可能不合法

后加入是因为while中是不合法解,while之前可能还是不合法,while之后才是合法解

                                                                                                        【在合法解中找答案!】 

总之 主要就是看什么时候加入元素能可能出现合法解 。

1-先加入新元素合法 就先加入新元素

2-先加入新元素有可能不合法 就先解决矛盾 后加入

代码如下:

class Solution {
public:string minWindow(string s, string t) {int min_len = s.size() + 1;//满足条件的解的长度int l = -1;//满足条件的解的起始位置unordered_map<char,int> v;//记录t中需要的字符 不能用bool因为可重复unordered_map<char,int> mp;//用于记录当前的窗口中的所有字符出现的情况for(int i=0;i<t.size();i++) v[t[i]] ++;//预处理vdeque<int> q;//滑动窗口int num=0;//当前窗口中的t中所需字符的数量for(int i=0;i<s.size();i++){q.push_back(i);//先加入 因为加入新元素后有可能满足条件if(v[s[i]] && mp[s[i]] < v[s[i]]) num++;//累加nummp[s[i]]++;//考虑到所有元素 因为要求最小值 不是t中的元素就可以出去 从而找到最小值while(!q.empty() && mp[s[q.front()]] > v[s[q.front()]]){//没有用的(多余的t中的元素的或t中没有出现的)元素就扔掉mp[s[q.front()]]--;q.pop_front();}if(num == t.size() && q.back() - q.front() + 1 < min_len){//只要有满足条件的解了就更新min_len = q.back() - q.front() + 1;l = q.front();}}return l > -1 ? s.substr(l,min_len) : "";}
};

后加入的例题:最长优雅子数组详解请看前几天的这个博客 。

代码如下:

class Solution {
public:int longestNiceSubarray(vector<int>& nums) {int ans=0,num=0;//num是当前窗口中的或运算deque<int> q;for(int i=0;i<nums.size();i++){while(!q.empty() && num & nums[i])//有交集 用按位或运算优化代码{num ^= nums[q.front()];//维护左端点 q.pop_front();}num |= nums[i];//或运算的优点:二进制位上只要有一个为1就为1(说明有人占位了 不能再占了 后续判断待判元素时就不需要一个一个按位与了 只需要对num来按位与即可 因为此时的num就已经是窗口中的所有元素按位或的结果了)q.push_back(i);//维护右端点 将待判元素入队//解决完矛盾之后就一定是合法解了 直接更新即可ans = max(ans,q.back() - q.front() + 1);}return ans;}
};

总结:

单调栈总结:

单调栈可以求出一个元素的一边的第一个大于或小于他位置,所以可以适用于求连续子数组的边界问题如力扣-接雨水、力扣-柱状图中的最大矩形、力扣-最大矩形等,再有就是一些比较明显的求一边的第一个大于或小于他的元素的位置了,就不过多赘述了。

滑动窗口总结:

滑动窗口分为两种:

1.双端队列实现:如果新元素与之前的元素没有冲突或影响,一般可以先入队,再维护左端点,但是如果新元素与之前的元素有了冲突(最长优雅子数组),就需要先维护左端点,直到没有冲突时再加入新元素,这样就能保证每次遍历中的队列都是一个满足题目要求的队列了,所以每次都更新最优解即可。

总之都是要先保证有解,再取最优解。 

2.单调队列优化:

一般的步骤就是先维护左端点,再通过维护右端点来维护队列的单调性,然后再加入新元素(一般用于找一个元素之前的最值)。

双端队列实现:队列这个窗口就是维护的满足条件的子数组区间,一般见于最短最长连续子数组

单调队列实现:队列中的是这个元素之前的单调小于或大于这个元素的值,而且队首就是最值。

/*
----------------=-=============================+++++++++++++++++++++++++++++**+*********************
-------------------==-=-=======================++=+++++++++++++++++++++++++++++*********************
:----------------------==========================+++++++++++++++++++++++++++++**+*******************
::-----------------------===-=-==========================+++++++++++++++++++++++++*+****************
:::::------------------------=====---::::...............:::::-==++++++++++++++++++++++*++***+*******
::::::-:-------------------==--::...............................::-=++++++++++++++++++*++++*++++++++
::::::::-:----------------:..........................................:-=++++++++++++++++++*+++++++++
::::::::::::-----------:.   ............................................:=+++++++++++++++++*+*******
:::::::::::::::------.    .............................::.................:-+++++++++++++++++++*****
::::::::::::::::-=:.   ...............................::...::::--:...........-+++++++++++++++++****+
:::::::::::::::--: .................................:-:..::..  .+*-:...........-++++++++++++++++++++
:::::::::::::-=:........:...........................-:..-.      +@#--:..........:+++++++++++++++++++
::::::::::::-=:.........::.............................==     .+%%%#.-:.........:-++++++++++++++++++
...::::::::--.....:--:::.::...........................:%%=::-*%%%%%%+ -:.......:---=++++++++++++++++
..::::::::--.....:+*. .::.::..........................+%%%%%%%%%%%%%%. -......:-----=+++++++++++++++
.......:.:=....:-=%=    ::............................*%%%%%%%%%%%%%%= :-.....:------=*+++++++++++++
.........=:....-:#%#.   =+...........:...............:#%%%%%%%%%%%#%%* .-.....:-------=*++++++++++++
........:=....:.=%%%#+=+%#:...........................#%#%==%%%%%%#%%*  -:....:--------+++++++++++++
........:-...:- +%%%%%%%%%:...........................+%*: -##%%*-*#%+  -:....:---------++++++++++++
........--...:: #%%%%%%%%%:...........................-#= .*:.###::#%-  -:....:---------++++++++++++
........-:...-. *%%#%%%%%%:............................+*=*#**####*#*...-:.....:--------=+=+++++++++
........-:...-. +%*.+@%#*#:.............................:------=====-::::......:---------*=+++++++++
........-:...-. :#-.+-##*#:.....................................................---------+===+++++++
........=:...::  +++*+#**=........::............................................---------+====++++++
........=:...:-..:*+=-::..........::..............::::::::::::::::::--==:.......:--------+====++++++
........-.....::::........::............:::--===+++++++++++++++*++++++**-.......---------+=====+++++
.......:-.........................::-==++++***+***+++++++++++++++++++++*+......:--------=*=======+++
.......-:............:::::::---==+++**+***++++++*++++++++++++++++++++++++:.....:--------=+=======+++
......:-.....::::-=+**########**+++++++*+++++++======--------------=+++++:.....:--------+=========++
......-:.....::-*##%%%#####%#*++++++++===----------------------------++++......--------++==========+
......:-........-#%########+====-------------------------------------=+*=.....:-------=+===========+
.......-:........:*%######*------------------------------------------+*+:....:-------==.:-+=========
.......:-.........:*%#####-------------------------------------------+*:.....:------=-:::.:++=======
........:-:........:+%##%*------------------------------------------++-.....:------=-..:::.-**++====
.........:-:.........=##%+-----------------------------------------++:....:------==---------++++*+++
...........--:........:*%#----------------------------------------+=:....:------==:::::::::...::::::
............:-::........-*+-------------------------------------==:....:------=-:...................
..............:--:.......:-=----------------------------------=--...::-----==-:.....................
................:--::.......:---------------------------------:...::-------=:..:....................
...................:--:.......::--------------------------::...::----=--:.-:........................
.....................:-=-:::.....:::----------------:::.....::--=---::...:-.........................
.................::::---..::::::::.......:::::.:......:::------::........-:.........................
.............:::::::-:.    ....::::::::::::::::::::------:::.............-:.........................
.........::::::...:-:      .............:::::::::........................=:.........................
......:::::......-:.  .    ..   .........................................=--::......................
...:--::........-:   ...       .   ......................................-=---::....................
:-:::.........:-:        ..   ..         ...........::::::::::::::::::::::+------:::................
::...........:-. .   . .        ........::.::::..::.:.................... :=---------::.............
............:-.        .......::........-......                       .    :=------------::::.......
...........:-. ......::::......        ::      ..           ..         . .  :=-----------------:::::
...........=:.:........      ..       .-.   ..          ..      .  .    .    .=---------------------
..........--...                ..     .- .        .                 .   .   . .:=-------------------
.........-=        .         . . .    :: ..     .    .   ..          .    ...   .-=-----------------
........:=.     .         .           -.                          .        ....  .::-=--------------
........-: .   ..  .   .         ..   -.                   ..    ... .      ..   ....::---=---------
*/

http://www.dtcms.com/a/276337.html

相关文章:

  • 11. JVM中的分代回收
  • 数据结构自学Day6 栈与队列
  • BaseDao 通用查询方法设计与实现
  • 快速过一遍Python基础语法
  • 015---全面理解交易:区块链状态转移的原子单位与链上执行全流程图解
  • 【AI News | 20250711】每日AI进展
  • APP Inventor使用指南
  • LeetCode 3169.无需开会的工作日:排序+一次遍历——不需要正难则反,因为正着根本不难
  • 【使用Pyqt designer时pyside2安装失败】
  • 如何彻底禁用 Chrome 自动更新
  • C++实现二叉树左右子树交换算法
  • vuecil3+版本下,兼容ie等不支持es6的低版本浏览器
  • 内容总监的效率革命:用Premiere Pro AI,实现视频画幅“一键重构”
  • 四、深度学习——CNN
  • 快速上手UniApp(适用于有Vue3基础的)
  • 服务器ssh连接防护指南
  • 软件测试基础1-软件测试需求分析
  • Python技巧记录
  • 详细理解向量叉积
  • CVPR2025 Mamba系列
  • 内容总结I
  • 我的LeetCode刷题笔记——树(2)
  • 带货视频评论洞察 Baseline 学习笔记 (Datawhale Al夏令营)
  • [动态规划]1900. 最佳运动员的比拼回合
  • Matplotlib 模块入门
  • 非欧几里得空间图卷积算子设计:突破几何限制的图神经网络新范式
  • Linux系统中部署Redis详解
  • python作业2
  • 【时间之外】AI在农机配件设计场景的应用
  • 【详解ProTable源码】高级筛选栏如何实现一行五列