单调队列,即单调递减或单调递增的
队列。使用频率不高,但在有些程序中会有非同寻常的作用。
单调队列的操作
举例
不妨用一个问题来说明单调队列的作用和操作:
不断地向缓存数组里读入元素,也不时地去掉最老的元素,不定期的询问当前缓存数组里的最小的元素。
最直接的方法:普通队列实现缓存数组。
进队出队都是O(1),一次查询需要
遍历当前队列的所有元素,故O(n)。
堆实现缓存数组
堆顶始终是最小元素,故查询是O(1)。
而进队出队,都要调整堆,是O(log(n))。
RMQ的方法
RMQ即Range Maximum(Minimum) Query,用来求某个区间内的最大值或最小值。使用
线段树或稀疏表是O(log(n))级的。对于这类问题这两种方法也搞得定,但是没有单调队列快。
单调队列的舞台
由于单调队列的队头每次一定最小值,故查询为O(1)。
进队出队稍微复杂点:
进队时,将进队的元素为e,从队尾往前扫描,直到找到一个不大于e的元素d,将e放在d之后,舍弃e之后的所有元素;如果没有找到这样一个d,则将e放在队头(此时队列里只有这一个元素)。
出队时,将出队的元素为e,从队头向后扫描,直到找到一个元素f比e后进队,舍弃f之前所有的。(实际操作中,由于是按序逐个出队,所以每次只需要出队只需要比较队头)。
每个元素最多进队一次,出队一次,摊排分析下来仍然是 O(1)。
上面的话可能还是没能讲出单调队列的核心:队列并不实际存在的,实际存在的是具有
单调性的子序列。对这个子序列按心中的队列进行操作,譬如在进队时丢弃的元素,虽然它不存在于这个子序列里,但是还是认为他存在于队列里。
另外,进队的顺序和出队的顺序并不一定相同,因为这个队列本身是隐含存在的,可以在进队时看成一个队列,出队时看成另一个队列,只要出队的元素在队列中就行。可以想象成一个队列只有头和身,另一个队列只有身和尾,而这身是共用的。
在OI赛场上,大多数题目为单调队列力所不能及的,取而代之的是单调队列基础上改进的斜率优化,单调栈等,因为其限制条件,故潜力不大。但需要掌握,因为有许多算法建立在其基础上。
例如斜率优化即为f[i] = min/max{f[j] + g[i] * g[j]},和单调队列尤为相似。
单调栈即为单调队列的“栈”版。
这两种复杂度也是O(n)的。
竞赛的应用
动态规划
f[x] = max or min{g(k) | b[x] ≤ k < x} + w[x]
(其中b[x]随x单调不降,即b[1]≤b[2]≤b[3]≤...≤b[n])
(g[k]表示一个和k或f[k]有关的函数,w[x]表示一个和x有关的函数)
这个方程怎样求解呢?注意到这样一个性质:如果存在两个数j, k,使得j ≤ k,而且g(k) ≤ g(j),则决策j是毫无用处的。因为根据b[x]单调的特性,如果j可以作为合法决策,那么k一定可以作为合法决策,并且k是一个比j要优的决策。因为k比j要优,(注意:在这个经典模型中,“优”是绝对的,是与当前正在计算的状态无关的),所以,如果把待
决策表中的决策按照k排序的话,则g(k)必然是不降的。在此例中决策表即f[x].
这样,就引导我们使用一个单调队列来维护决策表。对于每一个状态f(x)来说,计算过程分为以下几步:
1、 队首元素出队,直到队首元素在给定的范围中。
2、 此时,队首元素就是状态f(x)的最优决策,
3、 计算g(x),并将其插入到单调队列的尾部,同时维持队列的
单调性(不断地出队,直到队列单调为止)。
重复上述步骤直到所有的
函数值均被计算出来。不难看出这样的算法均摊
时间复杂度是O(1)的。因此求解f(x)的时间复杂度从O(n^2)降到了O(n)。
单调队列指一个队列中的所有的数符合单调性(单调增或单调减),在
信息学竞赛的一些题目上应用,会减少时间复杂度
例题1
【问题描述】
afy决定给TOJ印刷广告,广告牌是刷在城市的建筑物上的,城市里有紧靠着的N个建筑。afy决定在上面找一块尽可能大的矩形放置广告牌。我们假设每个建筑物都有一个高度,从左到右给出每个建筑物的高度H1,H2…HN,且0<Hi≤1,000,000,000,并且我们假设每个建筑物的宽度均为1。要求输出广告牌的最大面积。
【输入文件】
输入文件中的第一行是一个数n (n≤ 400,000 )
第二行是n个数,分别表示每个建筑物高度H1,H2…HN,且0<Hi≤1,000,000,000。
【输出文件】
输出文件 ad.out 中一共有一行,表示广告牌的最大面积。
【输入样例】
6
5 8 4 4 8 4
【输出样例】
24
【解释】各个测试点一秒,
但就这道题来说,n≤ 400,000,我们如果用枚举不会过全部数据,我们应设计出o(n)的算法来解决,这是单调队列就可以派上用场了。
具体做法是 先正着扫一遍,再倒着扫一遍,找到每一个数的
右极限与
左极限,最后找出最大值。
注意:这个是单调栈的做法,单调队列需要两遍均可弹出。
测试数据
样例输入1【ad.in】
20
12 8 8 30 40 32 19 22 12 32 30 45 15 1937 5 5 6 26 35
样例输出1 【ad.out】
144
样例输入2 【ad.in】
56
3000 2000 180 190 2890 2900 3120 450560 500 300 2100 2300 480 840 880 890 350 550 450 760 960 860 250 260 1050 11301140 2140 2045 2065 3075 3155 3255 3470 3490 3240 920 930 900 930 980 890 740760 770 825 845 855 950 1980 880 680 690 2380 2390
样例输出2【ad.out】
21080
例题2
【问题来源】
【问题描述】
有N个数,每次从左到右选取M个数,第一行选取每个区间中的最小值输出,第二行选取最大值并输出。
【解题思路】
这个是典型的固定k区间的单调队列。套用的本质思想是,如求最小值: 考虑这样的一个问题,在某个区间当中如果存在某两个元素A,B,满足A的下标小于B的下标,A的值大于B的值,那么A这个数就可以删掉,不再考虑。求最大值反之。
具体的操作是:从加入第k个数开始,每插入做一次队列
单调性更新:
删队尾【单调性】,入队,删队首【下标范围k以内】,输出队首【即最值】。
需要注意的是,这里用纯单调队列会超时,把删队尾的操作改成二分的话就过了。