单调队列及经典问题

217 阅读2分钟

RMQ 问题

  • 在讨论单调队列问题前,我们先看下 RMQ 问题
  • RMQ(x, y) 就是询问数组在区间 [x, y] 内的最小值
  • 现有数组如下,index 为数组索引值,value 为索引对应的元素值
    • index:0 1 2 3 4 5 6 7
    • value:3 1 4 5 2 9 8 12
  • 观察数组可以得到,RMQ(0, 3) = 1,RMQ(3, 7) = 2
  • 如果固定询问区间的尾部元素值,例如:RMQ(x, 7),那么最少记录上面数组中的几个元素,就可以满足任意元素值的 x 的 RMQ(x, 7) 需求
  • 由上述描述可以推算得出,最少记录 1,2,8,12 这四个元素即可满足
  • 因为不管元素 x 值为几,RMQ(x, 7) 的结果都在这四个元素中

单调队列

  • 很明显1,2,8,12是一个相对位置不变的单调递增的序列
  • 其实单调队列本质就是为了来维护区间最值问题的,也就是 RMQ 问题
  • 维护规则如下:
    • 入队操作:
      • 新元素从队尾入队,同时会将队列中已存在的,会破坏单调性的元素,从队尾依次移除,以维持队列元素的单调性
    • 出队操作:
      • 如果队首元素已经不在区间范围内,就将队首元素出队
    • 元素性质:
      • 队首元素始终是当前维护区间的最(大/小)值
  • 所以单调队列维护的元素,就是上述固定末尾的 RMQ 问题结果的最少元素

代码示例

  • 现有如下问题
  • 给出一个长度为 n 的数组 list,还有一个长为 k 的滑动窗口从数组最左移动到最右,每次窗口移动一个元素的距离,同时输出窗口中的最小值
  • 下面代码中
    • min 表示每次滑动窗口中的最小值组成的序列,定义单调队列 q,为方便计算队首元素是否超出窗口的范围,队列只用存对应元素的下标即可
    • 扫描每个元素,并进行入队操作,同时维护 q 的单调性
    • 如果滑动窗口滑过 队首元素指向的位置 刚好一格,则需要将队首元素进行出队操作
/**
 *
 * @param {*} list 原始数组
 * @param {*} k 滑动窗口的大小
 */
const slideWindow = (list, k) => {
  let min = []; 
  let q = [];

  for (let i = 0; i < list.length; i++) {
    // 维护 q 的单调性
    while (q.length && list[q.length - 1] > list[i]) q.pop();
    q.push(i);

    if (i - q[0] === k) q.shift(); 

    if (i + 1 < k) continue; // 此时还没有扫描到 k 个元素,还没扫描到窗口中的所有元素
    min.push(list[q[0]]);
  }

  console.log(min.join()); 
};

slideWindow([1, 3, -1, -3, 5, 3, 6, 7], 3); // -1,-3,-3,-3,3,3

小结

  • 上面简单介绍了 RMQ 问题,以及单调队列所维护元素的本质
  • 下篇文章,我们将介绍单调栈的相关知识点

“ 本文正在参加「金石计划 . 瓜分6万现金大奖」 ”