单调队列 - 兼 ACM PKU POJ 3250 及 2823 解题报告

[转] : http://blog.csdn.net/linulysses/article/details/5771084

  

单调队列

  假设序列 {xi }n = x1 ,x2 ,...,xn 中定义有一序关系 < (这里,也可以是 <=, >, >= 等,具体的是哪一种序关系视应用决定)。 那么,{xi }n 的一个单调队列为 {xi }n 的一个子序列 xj1 ,xj2 ,...,xjk ,其中,j1 < j2 < ... < jk,对任意 jp < jq,xjp < xjq 。这个性质为单调队列的单调性:下标的单调和元素之间的单调。

  和一般的队列类似,元素在队头出队,在队尾入队。所不同的时,在入队时,为了保证新元素入队后,该队列依然保持单调性,可能会使已经在队列内的某些甚至全部元素在队尾出队。例如,对于序列 {xi }n= { 1, 2, 6, 4, 0, 7 },当元素 4 入队前时,队列为 {1, 2, 6};元素 4 入列时,元素 6 将从队尾出队,从而得到新的单调队列 {1, 2, 4 }。而当元素 0 入队,队内原来的所有元素都出队,新的队列为 {0}。

  

    /* remove element with index before f from head of mono queue */  
DeQueue(MonoQ,f)
while MonoQ.head.index is ahead of f do
//do something here
remove MonoQ.head
end
end

  

 

    EnQueue(MonoQ,elem)  
while MonoQ.tail >= elem do
//do something here
remove MonoQ.tail
end
append elem to MonoQ
end

  

 

  从上述伪代码可以看出,每个元素最多只入队一次,最多也只出队一次(要么在队头,要么在队尾),因此,维护单调队列的时间复杂度为 O(n)。实现上,可以使用数组,也可以使用链表。do something here 的地方,可以进行一些聚合计算之类。一个重要的观察时,该计算一般只依赖于元素下标或即将出队的元素及即将入队的元素。如是,我们在出队元素时,只要进行一次与该元素相关的计算,即可安全地移除该元素。

 

应用 -  最大长方形

 

  在本人的一篇博文 最大长方形 IV中,虽然没有明确提到,但该算法中的栈,正是一个单调队列。元素的序关系由直方柱的高度决定。当新元素从队尾入队时,移除不比它低的直方柱,在移除的同时计算一个可能的解(上述伪代码中 do something here 的地方)。

 

应用 -  POJ 3250

 

  题意(非严格翻译):有 n 头牛头朝东站成一列。每头牛有一定的高度,并且能看到其前面高度比它低的牛的头顶,直到被某头高度大于等于它的高度的牛所挡住。计算每头牛能看到的牛头顶 的数量之和。参见:http://acm.pku.edu.cn/JudgeOnline/problem?id=3250

 

最简单的算法就是对每头牛都计算其能看到的数量,然后加起来得到答案,时间复杂度为 O(n2 )。对于本题,必然会 TLE。不可取。

 

  每头牛能看到的数量由其下标及其前面第一头不低于它的牛的下标决定。同时,为了计算某牛 x 能看到的数量,我们至少得等到第一头不低于它的牛 y 被读进来,这时,我们即可计算 x 能看到数量。假设 x 先于 y 被读进来(只要按顺序读取牛的高度即可),当 x 被读进来时,先于 x 且其数量的计算依赖于 x 的牛能看到的数量假设都已被计算(我们只需要每读进一头牛,就计算依赖它的牛所能看到数量),则我们可以安全的抛弃 x,而不影响后续的计算。这两点性质,和应用单调队列所需的性质相吻合,因此,可以尝试应用单调队列。

 

  现在就让我们来严格分析这个思路。cow[1..n] 存放牛的高度。一开始,单调队列 monoq 为空。考虑当前牛 i。如果队列为空,则入队。否则,如果队尾的牛 x 不高于 i,则令 x 出队,这时 i 为往东第一头不低于 x 的牛(否则,x 在此之前就应出队了),于是,x 能看到的牛的数量为 f(x) = i - x - 1。把 f(x) 叠加到 ans 上(初值为 0)。继续这个过程直到队列为空或队尾牛比 i 高。然后,将 i 入队。当考虑完所有的牛后,再将一头高度为无穷大的下标为 n+1 的牛入队,以保证所有的正常的牛都出队。

  

    ans := 0  
monoq := {}
cow[n+1] = infinity
for i from 1 to n+1 do
while monoq is not empty and monoq.tail.height <= cow[i] do
increase ans by (i-monoq.tail.index-1)
remove the tail element from monoq
end
append new element {height:cow[i],index=i} to tail of monoq
end
output ans

  

  这样,维护单调队列的复杂度为 O(n),每头牛都只扫描一遍,因此,总的时间复杂度为 O(n)。

应用 - POJ 2823

  前面介绍的两个应用实例都不涉及在队头出队的情况。而这道题,正好给我们以了解这一应用的机会。

题意:一个有 n 个元素的序列和一个宽为 k 的滑动窗口。滑动窗口从序列的左边滑到右边,每次滑行一个元素的距离。依次输出每个窗口的最小值和最大值。

显然,暴力算法的复杂度为 O(nk)。利用单调队列,复杂度为 O(n)。这里只讨论最小值,最大值的求法类似。大体的思路是:维护一个单调递增序列,使其保证队头元素落在当前窗口内。如是,则monoq.head为 当前窗口的解。每当窗口滑动时,入队新看到的元素,在队头删除已经落在窗口外的过时元素(由于窗口一直从左移到右,过时元素不会再被访问)。

  

    x[1..n] is the sequence  
mins[1..n-k+1] is the mins of windows from 1 to n-k+1
monoq := {}
// initialize the monoq for the first window
for i from 1 to k-1 do
while monoq is not empty and monoq.tail.value < x[i] do
remove the tail element of monoq
end
append new element {index:i, value:x[i]} to the tail of monoq
end
for i from k to n do
while monoq is not empty and monoq.tail.value < x[i] do
remove the tail element of monoq
end
append new element {index:i, value:x[i]} to the tail of monoq
//remove stale element from head
while monoq.head.index <= i-k do
remove the head of monoq
end
ans[i-k+1] := monoq.head.value
end

  

  需要指出的时,这个算法只对窗口长度为一固定值的情况成立。并且,适合于scheduled query,例如本题,需要对每个 窗口都计算最小值。如果是 ad hoc query 并且窗口长度会变化,则单调队列并不适合。这种情况下,可以用 RMQ 数据结构。

  注意:本文会持续更新本人遇到的单调队列的应用(对某些情况可能会做详细分析)。



你可能感兴趣的:(ACM)