算法学习12: 单调队列和单调栈

  • 单调队列
    • 单调队列解决的问题: 窗口内最大/最小值的更新结构
    • 单调队列的结构和操作
    • 单调队列的应用
      • 题目一: 生成窗口最大值数组[leetcode 239](https://leetcode.com/problems/sliding-window-maximum/)
      • 题目二: 统计极差小于定值的子数组个数
  • 单调栈
    • 单调栈解决的问题
    • 单调栈的结构
    • 单调栈的实现([leetcode 1019](https://leetcode.com/problems/next-greater-node-in-linked-list/))
      • 不考虑重复元素
      • 考虑重复元素
    • 单调栈的应用
      • 题目一: 直方图中的最大矩形[leetcode 84](https://leetcode.com/problems/largest-rectangle-in-histogram/)
      • 题目二: 最大子矩阵大小[leetcode 85](https://leetcode.com/problems/maximal-rectangle/)
      • 题目三: 一道很难想的题目

单调队列

单调队列解决的问题: 窗口内最大/最小值的更新结构

滑动窗口窗口左边界窗口右边界都只能向右移动,且保证窗口左边界永远小于窗口右边界,如何在常数时间内找到窗口中的最大值?

单调队列的结构和操作

单调队列中储存的是有可能成为窗口最大值元素对应的下标索引及其值,每次窗口范围发生变化,都要对单调队列进行更新. 单调队列保证对应元素的值是严格递减的.

单调队列的维护基于如下事实:

越靠右(晚被淘汰)且值越大的元素,越有资格成为窗口内的最大值.

  • 初始时,窗口长度为0,窗口内没有元素,单调队列中也没有元素.
  • 窗口右边界向右扩展一位时,有新的元素加入窗口,将新加入的元素与单调队列末尾元素进行比较:
    • 新加入窗口的元素小于队列末尾元素: 当队列前边元素都出窗口后,该新元素有可能成为数组最大值,因此将其加到单调队列末尾.
    • 新加入窗口的元素大于队列末尾元素: 比起前边所有比新元素值小或相等的元素,新元素的值更大,且更后出窗口,因此它比前边所有比它小或相等的元素更有资格成为最大值,因此我们将前边所有值小于等于这个元素的元素全部弹出,然后将新元素加入到单调队列末尾.
  • 窗口左边界向右扩展一位时,窗口元素个数减一,因此我们要判断单调队列中是否有元素已经失效了. 因为单调队列中元素下标时严格递增的,因此只需要考察单调队列队首元素是否出窗口了,若出窗口则将其从队列中弹出.
  • 若在任意时刻,要求窗口最大值,只需返回单调队列的队首即可.

单调队列中可以只储存数组下标,其值可以通过数组索引得到.
实际编程时要注意别一不小心存成了数组的值.

// 初始化单调队列
Deque<Integer> qmax = new LinkedList<>();  // 单调队列,存储的是数组下标
int leftIndex = 0, rightIndex = 0;            // 滑动窗口的左右边界// 窗口左边界右移操作: 判断队首元素是否过期了
leftIndex++;
if (!qmax.isEmpty() && qmax.peekFirst() < leftIndex) {qmax.pollFirst();
}// 窗口右边界右移操作: 弹出队尾所有小于等于当前元素值的元素,再将当前元素加入
rightIndex++;
while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[rightIndex]) {qmax.pollLast();
}
qmax.addLast(rightIndex);// 获取当前窗口最大值操作: 返回单调队列首元素
int maxElememt = arr[qmax.peekFirst()];

单调队列的应用

题目一: 生成窗口最大值数组leetcode 239

问题: 一个固定长度的滑动窗口在一个数组上滑动,要求返回窗口滑动到每一个位置时窗口内的最大值
解答: 直接应用单调队列,将窗口在数组上滑动的同时更新对应的单调队列.

题目二: 统计极差小于定值的子数组个数

问题: 给定一个数组,统计其极差(最大值与最小值之差)小于num子数组(子数组要求连续)的个数.
解答:

  • 该问题的解有一个很好的性质:

    • 如果一个子数组符合要求,则其中任何一个子数组也符合要求.
    • 如果一个子数组不符合要求,则任何包含它的子数组亦不符合要求.
  • 因此我们的解法如下
    • 初始时L=0,R从0开始向右扩张,直到不符合要求.
    • 若R扩张到不符合要求了,则将L右移一位,R从上一个达标处开始扩张.
    • 用一个单调队列存储窗口的最大值和最小值.

单调栈

单调栈解决的问题

对一个数组中的每一个数返回其左边第一个比它大的值右边第一个比它大的值,在O(N)时间内找到.

单调栈的结构

构造一个单调栈,保证栈的结构:从栈底到栈顶其元素是逐渐缩小的.先将所有数字依次压栈,再将栈弹出.

  • 将数组每一位依次入栈,入栈时为了维护单调栈从底到顶递减的结构,进行必要的出栈

    • 当前元素小于栈顶元素,则将该元素压入栈,不违背从栈底到栈顶递减的结构.
    • 当前元素大于等于栈顶元素,则当前元素比栈顶元素更有资格成为右侧元素的最近左较大值,因此不断出栈直到栈顶元素小于当前元素.对出栈元素更新其左临近更大值右临近更大值
      • 出栈元素是因为遇上了当前元素才出栈,因此其右临近更大值当前元素
      • 出栈元素左临近更大值为其栈中向底的下一个元素.
  • 当所有元素都已入栈了,则再将栈中元素依次弹出,并对出栈元素更新其左临近更大值右临近更大值
    • 出栈元素并不是因为右边任何一个元素而被弹出,因此其右临近更大值为空
    • 出栈元素左临近更大值为其栈中向底的下一个元素.

单调栈的实现(leetcode 1019)

不考虑重复元素

若不存在重复元素,则下边程序是对的,但若数字有重复的,则下边程序会在左临近更大值数组右临近更大值数组其中之一产生错误.

  • 若判断弹出条件为nums[i] >= nums[stack.peek()],则重复元素的右临近更大值数组出错(会导致下图中B,C,D左临近更大值均为A,右临近更大值分别为C,D,E)
  • 若判断弹出条件为nums[i] > nums[stack.peek()],则重复元素的左临近更大值数组出错(会导致下图中B,C,D右临近更大值均为E,左临近更大值分别为A,B,C)

考虑到重复元素,并要求左临近更大值数组右临近更大值数组均正确,只要遍历两遍数组,第一次保证左临近更大值数组正确,第二次保证右临近更大值数组正确.

int[] leftMore = new int[nums.length];  // 左临近更大值数组
int[] rightMore = new int[nums.length]; // 右临近更大值数组
Arrays.fill(leftMore, -1);              // 若左边没有最近的小于它的数,则为-1
Arrays.fill(rightMore, nums.length);    // 若右边没有最近的小于它的数,则为heights.lengthStack<Integer> smin = new Stack<>();    // 单调栈中存放的是数组的下标值,要求栈中元素对应的值递减// 将所有元素插入单调栈
for (int i = 0; i < nums.length; i++) {// 易懂写法// if (smin.empty() || heights[i] < heights[smin.peek()]) {//     // 若当前值小于栈顶值,满足元素递减原则,直接压入栈//     smin.push(i);// } else {//     // 若当前值大于等于栈顶值,则弹出//     while (!smin.empty() && heights[i] >= heights[smin.peek()]) {//         int cur = smin.pop();//         // cur是因为i而被弹出,因此其右边较小值为i,其左边最小值为栈中向底一项//         rightMin[cur] = i;//         leftMin[cur] = smin.empty() ? -1 : smin.peek();//     }//     // 直到满足递增结构,再将对应元素插入//     smin.push(i);// }// 简洁写法// 若当前值小于栈顶值,满足元素递减原则,则不进入循环,直接压入栈// 若当前值大于等于栈顶值,则不断弹出栈顶直到其满足元素递减原则while (!smin.empty() && nums[i] >= nums[smin.peek()]) {  // 此处进入循环条件不同导致错误的结果不同int cur = smin.pop();// cur是因为i而被弹出,因此其右边较小值为i,其左边最小值为栈中向底一项rightMore[cur] = i;leftMore[cur] = smin.empty() ? -1 : smin.peek();}// 直到满足递减结构,再将对应元素插入smin.push(i);
}// 将单调栈中元素弹出
while (!smin.empty()) {int cur = smin.pop();rightMore[cur] = nums.length;leftMore[cur] = smin.empty() ? -1 : smin.peek();
}

考虑重复元素

考虑到重复元素,则将单调栈中存储的元素由下标值改为下标数组

单调栈的应用

题目一: 直方图中的最大矩形leetcode 84

问题: 给出一个直方图,再直方图中找到面积最大的矩形,要求时间复杂度O(N)

解法: 把直方图看成一系列棍的集合.
对于每一根棍,找到其两边的最近的高于它的棍,矩形面积=棍高度*两边连续的更高棍个数. 其两边最近的高于它的棍可以用单调栈来求.
对于每一个棍,都求出其对应的矩形面积,最终计算得到其最大面积.

这道题若使用不考虑重复元素的写法,会有一些棍的最近的高于它的棍求解错误,但由上边代码处分析可知,对于多个重复高度的棍,总会有一根棍的左右最近高于它的棍求取均是正确的,这对本道题目就已经足够了.

题目二: 最大子矩阵大小leetcode 85

问题: 给出一个矩阵,其值为0或1,找到面积最大的全1矩阵面积.
如下矩阵:
[10100101111111110010]\left[ \begin{matrix} 1 & 0 & 1 & 0 & 0 \\ 1 & 0 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 0 & 0 & 1 & 0 \end{matrix} \right] ⎣⎢⎢⎡​1111​0010​1110​0111​0110​⎦⎥⎥⎤​
其面积最大的全1矩阵面积为6
解法: 这个问题应用到了上边问题(直方图中最大矩形)作为子问题.

题目三: 一道很难想的题目

问题: 给出一个环形数组,表示一个环形的烽火台,烽火台之间互有遮挡
有两种条件下两个烽火台可以互相看见

  1. 相邻的两个节点可以互相看见
  2. 两个节点之间沿圆周组成的优弧劣弧之间所有的节点都小于这两个节点,则这两个节点可以互相看见

如下边圆周之上有9组能互相看见的节点.

简单问法: 请问n个两两不相同的组成的数组之间一共有几组能互相看见的节点? 在O(1)时间内给出

解答: 分情况讨论

  1. n<=1, 共有0对两两互相看见的节点
  2. n=2, 共有1对两两互相看见的节点
  3. n>=3, 共有2*i-3对两两互相看见的节点

其中第1,2种情况易知,第三种情况证明如下
对于每一组互相看见的节点对,都有一个较小值和一个较大值,我们希望确定较小值取找较大值.
找出最大值次大值,将除此两个值之外的n-2个值各自作为较小值,都能沿着顺逆两个方向找到较大值且两个较大值互不相同,有2*(n-2)组互相看见的节点,除此之外最大值最小值互相能看见.因此共有2*n-3个可以互相看见的节点对.

困难问法: 若n个节点之间有可能存在相同值,其组成的环形数组共有几个能互相看见的节点?

解答: 与上问类似,我们站在较低节点上去找较高节点,本质上是在找节点是否存在左临近较大值右临近较大值,因此用到单调栈结构.
不同的是这个单调栈要考虑到存在重复值,因此栈中存储的是{下标, 相等元素出现次数}结构,要涉及到节点的合并.

  • 先找到数组的一个最大值(若有多个相等最大值则任选一个),从这个节点开始进行构造单调栈.(后面会解释为什么选这个节点作为开始).在上图中从5(起点)开始.

  • 先将节点依次入栈,在将所有节点入栈过程中会发现出栈的情况. 这说明出栈节点存在逆时针临近最大值,这时可以以出栈节点较小值,以临近节点较大值,找到互能看见的节点.

    • 出栈节点在单调栈中记载的出现次数等于1,则说明从这个节点沿着顺时针和逆时针方向看去,都能找到一个临近更大值,且这两个临近更大值会挡住其它的更大值.因此此节点带来了2个新的互能看见的节点.

      例如:遍历到红色4时,节点{值为3,出现1次}被弹出,其顺时针,逆时针方向上都有一个更大值,带来[5,3],[4,3]两对互能看见节点

    • 出栈节点在单调栈中记载的出现次数k大于1,这些节点沿着顺逆时针方向各自都能找到临近更大值,带来2*k互能看见的节点,且这几个出栈节点之间互能看见,带来C(k,2)互能看到的节点,共带来C(k,2)+2*k互能看到的节点.

      例如: 若遍历到黄色5时,节点{值为4,出现4次}被弹出,共带来[4,5].[5,4],和6个[4,4],共带来8个新的互能看到的节点

    • 这里可以看到,为什么选取数组最大值开始作为遍历起点,的原因,因为只有这样才能保证数组中所有节点出栈时沿着顺时针方向都能找到一个顺时针临近最大值.

  • 遍历完成之后,单调栈中还存在节点,我们要将其全部出栈,出栈时考虑带来了几个胡能看到的节点.

例如: 对于上边的数组来说,遍历结束时栈中存在节点[{值为5,出现3次}, {值为4,出现2次}, {值为3,出现3次}]

  • 出栈节点不是栈中倒数第一个倒数第二个节点.设出栈节点出现次数为k,则这k个节点在顺逆时针方向都能找到临近较大值,则新带来的互能看到的节点C(k,2)+2*k个.
  • 若弹出节点是栈中倒数第一个节点,这时候要考虑到栈底节点到底出现几次,设出栈节点出现次数为k.
    • 出栈节点的出现次数**>=2**,则说明顺逆方向都各自能能找到临近较大值,则新带来的互能看到的节点C(k,2)+2*k个.
    • 若栈底节点的出现次数**=1**,则说明顺逆方向会找到同一个临近较大值,则新带来的互能看到的节点C(k,2)+1*k个.
  • 出栈节点栈底节点,设出栈节点出现次数为k.则其在顺逆方向上都找不到临近较大值,这k个节点之间互能看见,则新带来的互能看到的节点k==1 ? 0 : C(k,2)个.

代码如下:

import java.util.Stack;class Solution {// 定义Pair类,用于记录节点值及其出现次数public static class Pair {public int value;public int times;public Pair(int value) {this.value = value;this.times = 1;}}// 辅助函数,找到循环数组下一个位置的索引public static int nextIndex(int size, int i) {return i < (size - 1) ? i + 1 : 0;}// 辅助函数,计算n个相等节点之间产生的 互能看见节点个数public static long getInternalSum(int n) {return n == 1L ? 0L : (long) n * (long) (n - 1) / 2L;}// 辅助函数,找到数组最大节点对应的下标public static int getMaxIndex(int[] arr) {int maxIndex = 0;for (int i = 0; i < arr.length; i++) {maxIndex = arr[maxIndex] < arr[i] ? i : maxIndex;}return maxIndex;}// 核心函数,计算 互能看见节点个数public static long communications(int[] arr) {// 判空if (arr == null || arr.length < 2) {return 0;}int size = arr.length; // 数组长度int maxIndex = getMaxIndex(arr); // 数组最大值下标,作为压栈起点int value = arr[maxIndex]; // 临时变量,当前遍历到的节点的值int index = nextIndex(size, maxIndex);// 临时变量,当前遍历到的节点的下标long res = 0L; // 累加 记录 互能看见节点个数Stack<Pair> stack = new Stack<>(); // 单调递减栈stack.push(new Pair(value));// 将所有节点压入单调栈while (index != maxIndex) {value = arr[index];// 为了维护栈的递减结构,弹出所有前边小于当前值的节点while (!stack.isEmpty() && stack.peek().value < value) {int times = stack.pop().times;// 计算 出栈节点之间 以及 出栈节点和临近较大节点之间 产生的 胡能看到节点个数res += getInternalSum(times) + times;res += stack.isEmpty() ? 0 : times;}// 将当前节点入栈(有可能合并节点)if (!stack.isEmpty() && stack.peek().value == value) {stack.peek().times++;} else {stack.push(new Pair(value));}// 遍历指针后移index = nextIndex(size, index);}// 入栈完毕,将剩余节点出栈while (!stack.isEmpty()) {int times = stack.pop().times;// 判断弹出的是从栈底开始第几个节点int stackSize = stack.size();if (stackSize > 1) {// 弹出的是 从栈底开始第三个 以及 其上 的节点res += getInternalSum(times) + 2 * times;} else if (stackSize == 1) {// 弹出的是 从栈底开始第二个节点res += getInternalSum(times) + (stack.peek().times > 1 ? 2 * times : times);} else if (stackSize == 0) {// 弹出的是最后一个节点res += getInternalSum(times);}}return res;}
}

算法学习12: 单调队列和单调栈相关推荐

  1. 蒟蒻的ACM数据结构(四)-单调队列和单调栈

    单调队列和单调栈 一.概念 二.实现 三.题目 单调队列 洛谷P1886 滑动窗口 解析 单调栈 [GXOI/GZOI2019]与或和 解析 POJ3250 Bad Hair Day 解析 POJ 2 ...

  2. 算法学习1:定容字符串栈的Java实现

    算法学习1:定容字符串栈的Java实现 代码 import java.io.File; import java.io.FileNotFoundException; import java.util.S ...

  3. 算法竞赛入门与进阶 (二)单调队列、单调栈

    栈(stack)和队列( queue ) 1.栈的定义:栈是限定仅在表头进行插入和删除操作的线性表(先进后出) 2.队列的定义:队列是一种特殊的线性表,特殊之处在于 它只允许在表的前端(front)进 ...

  4. 单调队列,单调栈总结

    最近几天接触了单调队列,还接触了单调栈,就总结一下. 其实单调队列,和单调栈都是差不多的数据类型,顾名思义就是在栈和队列上加上单调,单调递增或者单调递减.当要入栈或者入队的时候,要和栈头或者队尾进行比 ...

  5. 单调队列以及单调队列优化DP

    单调队列定义: 其实单调队列就是一种队列内的元素有单调性的队列,因为其单调性所以经常会被用来维护区间最值或者降低DP的维数已达到降维来减少空间及时间的目的. 单调队列的一般应用: 1.维护区间最值 2 ...

  6. BFS广度优先算法, DFS深度优先算法,Python,队列实现,栈实现

    来源:https://www.bilibili.com/video/BV1Ks411575U/?spm_id_from=333.788.videocard.0 BFS广度优先算法 graph = {& ...

  7. 算法学习-单调双端队列

    文章目录 基础知识 算法模板 相关题目 239.滑动窗口最大值 1438.绝对差不超过限制的最长连续子数组 862.和至少为K的最短子数组 1425.带限制的子序列和 1499.满足不等式的最大值 2 ...

  8. 算法学习 (门徒计划)4-2 单调栈(Monotone-Stack)及经典问题 学习笔记

    算法学习 (门徒计划)4-2 单调栈(Monotone-Stack)及经典问题 学习笔记 前言 单调栈 基础 性质 代码实现 总结 经典例题 LeetCode 155. 最小栈 (基础) 解题思路 L ...

  9. 数据结构录 之 单调队列单调栈。

    队列和栈是很常见的应用,大部分算法中都能见到他们的影子. 而单纯的队列和栈经常不能满足需求,所以需要一些很神奇的队列和栈的扩展. 其中最出名的应该是优先队列吧我觉得,然后还有两种比较小众的扩展就是单调 ...

最新文章

  1. PHPStudy 安装 Imagick 报错:无法定位程序输入点 于动态链接库上
  2. eclipse中各种查找
  3. openstack登陆dashboard提示认证发生错误
  4. simple css 汉化,Simple CSS(CSS文档生成器)
  5. 一个简单的记事本程序
  6. php 数组元素快速去重
  7. 01.ShardingSphere笔记
  8. Libra教程之:来了,你最爱的Move语言
  9. JavaScript数据结构与算法——链表详解(下)
  10. Linux协议栈:基于ping流程窥探Linux网络子系统,及常用优化方法
  11. Character,String相关方法,Int,double互相转换
  12. eks volumn s3_和平精英:SMG战队无缘总决赛,S3前提退场原因一览
  13. GSM掉话原因(网优的基础知识)
  14. window.onscroll页面滚动条滚动事件
  15. 微信支付将为O2O画上句号
  16. 小莫取色精灵 使用教程_MQ
  17. 制造业信息化的伴侣---Windows 2016超融合
  18. java calendar星期_作业-用Calendar获取今天是星期几
  19. 吃了知乎月饼,成了「喷射战士」
  20. unity 3d水的资源包_使用Apple LiDAR,一小时为你家量身打造3D游戏

热门文章

  1. 渗透测试以及安全面试的经验之谈-技术篇
  2. 中国石油大学《工程热力学与传热学》第三阶段在线作业
  3. ​为什么大多数代码都很糟糕,能做些什么来改进代码吗?
  4. 这些年,我“端”掉的软件测试培训机构
  5. 本地缓存需要高时效性怎么办_唯品会三年,我只做了5件事,如今跳槽天猫拿下offer(Java岗)...
  6. power bi数据分析_Power BI数据模型:使用关系
  7. Lodop打印表格包含页眉和页码
  8. 兰亭集势Q1业绩优异,难掩股价颓势,市值缩水五成
  9. livox mid360接线制作
  10. php微信登录代理转发,PHP微信网页授权登录