alg: 单调队列和滑动窗口算法

单调队列

给定一个长度为 n 的数组,需要输出每连续 k 个数(A[i, i+k-1])中的最大值,可以在 O(n) 时间内做完吗?

一个思路就是动态地维护一个队列,在任何时刻 i 都满足

  • 队列中的元素来自 A[i, i+k-1]
  • A[s] 在队列中,当且仅当 A(s, i+k-1] 中的元素都小于 A[s]。特别地,A[i+k-1] 一定在队尾。

不难看出,“对头”或队列的最左侧元素就是 A[i, i+k-1] 的最大值。

每次增加 i,仅需:

  • 从原队列的队尾开始逐个将不大于 A[i+k] 的元素弹出,然后让 A[i+k] 进队尾
  • 检查对头的元素是否是 A[i],如果是,将其弹出

每个元素最多进队、出队一次,因此算法是 O(N) 的。

滑动窗口

滑动窗口算法有诸多类型,如定长窗口、求最短窗口、求最长窗口等等。

用双指针 lp, rp 来把当前窗口定义为 A[lp, rp),这样,移动左端和右端的操作就都很简单:

add A[rp] to window
rp = rp + 1
remove A[lp] from window
lp = lp + 1

定长窗口

给定一个长度为 n 的数组,需要输出每连续 k 个数(A[i, i+k-1])中第 x 小的数,复杂度可以多小?相关的例题:Leetcode 2653

一个思路是,维护两棵平衡搜索树来存储窗口中的数,第一棵树大小为 x,第二棵树大小为 k-x,保证任何时刻第一棵树中的元素都小于第二棵树。这样,第一棵树的最大值就是窗口中第 x 小的数了。

每次移动窗口,需要删除一个元素,增加一个元素:

  • 如果删除的元素在第一棵树中,增加的元素小于第二棵树的最小元素,则把增加的元素插入第一棵树。对称的情况同理。
  • 如果删除的元素在第一棵树中,增加的元素大于第二棵树中最小的元素,则把第二棵树最小的元素转移到第一棵树中,在第二棵树中插入增加的元素即可。对称的情况同理。

这样做,需要 O( n (log(x)+log(k-x)) ) 的复杂度。使用堆(优先级队列)并不会减少其复杂度,尽管可能提升其实际性能。

如果使用全序向量的接口,那么这个问题就直接解决了:每次移动窗口增加一个、删除一个元素(各用时 O(k^{1/3})),然后读取第 k 小的数用时 O(1),共用时 O(n k^{1/3})。

求最大窗口

如果一个窗口满足条件,则其所有子窗口都满足条件。特别地,长度为1的窗口都满足条件。

# Must be able to add any element when window is empty
lp, rp = 0, 0
while rp < len(A)
  while cannot add A[rp] to window
    remove A[lp] from window
    lp = lp + 1
  add A[rp] to window
  rp = rp + 1
  A[lp, rp) is a new valid window

例:无重复字符 / 每个字符最多出现 k次 的最长子串

求最小窗口

如果一个窗口满足条件,则包含该窗口的任何窗口都满足条件。特别地,空窗口不满足条件。求最小窗口。

# Empty window (lp == rp) must not satisfy condition
lp, rp = 0, 0
while True:
  while rp < len(A) and condition is not satisfied
    add A[rp] to window
    rp = rp + 1
  if condition is not satisfied
    break # we reached the end
  while lp < rp and condition is satisfied
    A[lp, rp) is a new valid window
    remove A[lp] from window
    lp = lp + 1

例:Flowerpot(洛谷 P2698)给定一系列平面上的点 (x,y),求最短的 x 区间使得该区间上的点 y 坐标极差不小于 D


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *