zoukankan      html  css  js  c++  java
  • 单调队列

    单调队列,顾名思义就是具有单调性的队列O(∩_∩)O~,一般的队列只能从队尾入队、队首出队;为了保持单调队列的单调性,单调队列除具有这两种性质外,还可以从队尾出队。
    以单增的单调队列为例,当元素t要入队时,先要从队尾依次弹出所有>=t的元素,再将t加在队尾。
    举个例子,如果序列:1 3 -1 -3 10要构成单调队列,
    先将元素“1”放入队列中,以初始化队列,
    接着元素“3”要入队,队尾元素“1”比“3”小,因此“3”可以直接入队,队列变为1 3,
    接着“-1”要入队,从队尾依次弹出元素“3”“1”后将“-1”入队,队列变为-1,
    同理“-3”入队后,队列变为-3,
    “10”入队后,队列变为-3 10

    单调队列有什么用呢?看一道例题:(poj2823)
    给定含有n个元素的无序序列a[],和一个整数k,要求求出a[]中每连续k个元素组成的序列中的最小值(或最大值),这样的值可能有1个或n-k+1个。
    比较简单的方式,是每次都将k个数的最值找出,具有O(K*n)的时间复杂度。但如果用单调队列的话,我们可以在O(n)的时间内求解,原因是每个元素最多入队一次、出队一次。
    要解决该题,我们还要记录每个元素在原序列中的位置p,每次只需从队首开始找到跟当前元素a[i]距离不大于k的元素(即是i-p+1<=k)输出即可。

    一、 什么是单调(双端)队列
    单调队列,顾名思义,就是一个元素单调的队列,那么就能保证队首的元素是最小(最大)的,从而满足动态规划的最优性问题的需求。
    单调队列,又名双端队列。双端队列,就是说它不同于一般的队列只能在队首删除、队尾插入,它能够在队首、队尾同时进行删除。
    【单调队列的性质】
    一般,在动态规划的过程中,单调队列中每个元素一般存储的是两个值:
    1、在原数列中的位置(下标)
    2、 他在动态规划中的状态值
    而单调队列则保证这两个值同时单调。
    从以上看,单调队列的元素最好用一个类来放,不这样的话,就要开两个数组。。。

    单调队列:单调队列 即保持队列中的元素单调递增(或递减)的这样一个队列,可以从两头删除,只能从队尾插入。单调队列的具体作用在于,由于保持队列中的元素满足单调性,对手元素便是极小值(极大值)了。

    单调队列的舞台

    由于单调队列的队头每次一定最小值,故查询为O(1)。
    进队出队稍微复杂点:
    进队时,将进队的元素为e,从队尾往前扫描,直到找到一个不大于e的元素d,将e放在d之后,舍弃e之后的所有元素;如果没有找到这样一个d,则将e放在队头(此时队列里只有这一个元素)。
    出队时,将出队的元素为e,从队头向后扫描,直到找到一个元素f比e后进队,舍弃f之前所有的。(实际操作中,由于是按序逐个出队,所以每次只需要出队只需要比较队头)。
    每个元素最多进队一次,出队一次,摊排分析下来仍然是 O(1)。
    上面的话可能还是没能讲出单调队列的核心:队列并不实际存在的,实际存在的是具有单调性的子序列。对这个子序列按心中的队列进行操作,譬如在进队时丢弃的元素,虽然它不存在于这个子序列里,但是还是认为他存在于队列里。
    另外,进队的顺序和出队的顺序并不一定相同,因为这个队列本身是隐含存在的,可以在进队时看成一个队列,出队时看成另一个队列,只要出队的元素在队列中就行。可以想象成一个队列只有头和身,另一个队列只有身和尾,而这身是共用的。

    一、单调队列的直接应用

    1.合并果子

    【问题描述】
    在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。每一次合 并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总 共消耗的体力等于每次合并所耗体力之和。
    因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
    例如有3种果子,数目依次为1,2,9。可以先将 1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为 12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
    【输入文件】
    输入文件fruit.in包括两行,第一行是一个整数n(1 <= n <= 10000),表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数ai(1 <= ai <= 20000)是第i种果子的数目。
    【输出文件】
    输出文件fruit.out包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于231。
    【样例输入】
    3
    1 2 9
    【样例输出】
    15
    【数据规模】
    对于30%的数据,保证有n <= 1000;
    对于50%的数据,保证有n <= 5000;
    对于全部的数据,保证有n <= 10000。

    分析:

    这个题目非常的经典,发放也很多,可以采用快排或者堆,其思想都是选取当前最小的两个堆进行合并。复杂度均为O(nlogn),如果用有序队列维护,时间复杂度为O(n)。

    每次选取进行合并的两堆,不是最先给定的堆,就是合并最初堆若干次后得到的新堆,所以需要维护两个单调递增队列,一个队列存最初给定的堆的值(1),一个存合并后得到的新值(2)。

    每次选择时有三种状态:

    1.选取队一的队首两个

    2.选取队2的的队首两个

    3.选取二者队首各一个

    只需对每个队列的指针做相应的更改。

    特别注意初始化。

    这道题很好的运用了题目中决策的单调性,对初始对经行排序,保证了其单调性。而对于新产生的堆来说,一旦有新元素加入其中,则新元素一定大于原有元素。(很显然,由于队列1的单调性)。

    也就是说,队列的单调性是自然而然的。是不需要维护的。要善于观察分析,才能发现。

    2.Window

    给定你n个数ai~an一个确定长度的区间,让你求出每个区间内的最大值,并按照顺序输出

    输入

       N,k

       A1~an

    输出

      每个区间内的最大值

    分析

      由于该题的区间长度一定,我们可以用一个长度为k(A,B)的队列来维护数据的有序性。

      剩下的问题就是如何维护队列的有序性了。

      最前面已经说到了,代码也不再赘余。

    3、志愿者选拔(foj  1894)

    世博会马上就要开幕了,福州大学组织了一次志愿者选拔活动。

    参加志愿者选拔的同学们排队接受面试官们的面试。参加面试的同学们按照先来先面试并且先结束的原则接受面试官们的考查。

    面试中每个人的人品是主要考查对象之一。(提高人品的方法有扶老奶奶过街,不闯红灯等)

    作为主面试官的John想知道当前正在接受面试的同学队伍中人品值最高的是多少。于是他请你帮忙编写一个程序来计算。

     Input

    输入数据第一行为一整数T,表示有T组输入数据。

    每组数据第一行为”START”,表示面试开始

    接下来的数据中有三种情况:

    1 C NAME RP_VALUE 名字为NAME的人品值为RP_VALUE的同学加入面试队伍。(名字长度不大于5,0 <= RP_VALUE <= 1,000,000,000)

    2 G 排在面试队伍最前面的同学面试结束离开考场。

    3 Q 主面试官John想知道当前正在接受面试的队伍中人品最高的值是多少。

    最后一行为”END”,表示所有的面试结束,面试的同学们可以依次离开了。

    所有参加面试的同学总人数不超过1,000,000

    Output

    对于每个询问Q,输出当前正在接受面试的队伍中人品最高的值,如果当前没有人正在接受面试则输出-1。

    Sample Input

    2

    START

    C Tiny 1000000000

    C Lina 0

    Q

    G

    Q

    END

    START

    Q

    C ccQ 200

    C cxw 100

    Q

    G

    Q

    C wzc 500

    Q

    END

    Sample Output

    1000000000

    0

    -1

    200

    100

    分析:

    题目本身就是队列,由于要找的是最大值,我们自然想到用单调队列解决问题。

    维护一个单调递减序列,只需输出序列中的第一个元素即可。

    对于命令我们可以进行不同的处理:

    如果是Q命令,则判断当前队列中是否仍有元素,如果没有则输出-1,如果有则直接输出队首。

    如果是G命令,则对last加1,之后对于队列中所有超出范围的前端元素进行出队操作。(该元素在原序列中的位置>=last)

    如果是C命令,则将该元素加入队列中,并和队尾元素比较,维护队列的单调性。

    这里考虑一个问题,当前元素加如后对队尾元素为什么可以毫无保留的删去呢?

    因为当前加入的元素比队尾元素大,且该元素比队尾元素入队晚(也就是该元素比队尾元素晚出队),所以只要该元素在队列中,就一定不会选取队尾元素。也就是当前状态一定比队尾元素的状态更优。——这里一定要理解深刻,这是队列的本质。

    因此,这题的单调队列中维护的一个属性是元素的价值,一个属性是单调队列中的元素在原序列中的位置。

    注意,q中的值是该元素在原序列中的位置!

    4、广告印刷

    【问题描述】

      最近,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是400000,我们只能用O(n)或O(nlogn)的算法。可以使用rmq,在后边的论文中会讲到。

    现在讲时间复杂度为o(n)的单调队列的方法。

    继续上边的思路,对于每个建筑物,只需要找到其能够扩展到的最大宽度即可。也就是这个建筑物的左右两边的比它低或等于它的建筑物个数。

    如何用单调队列呢?

    我们从1~n一次进队,维护一个单调递减序列。每次加入元素后维护其单调性,当然这样做必然会使一些元素出队,出队的元素一定要比当前加入的元素小,也就是说当前元素就是出队的元素能在右侧达到的最远的建筑物!

    注意,要让h[n+1]=0并且让该元素入队一次(会使当前队列中的所有元素出队),保证每个元素都有其“右极限”的值。

    要求“左极限”同理,只需从n~0循环即可,注意0

    这道题是对单调队列的变形使用。由于问题的结果具有单调性,很好的利用出队元素的特性.

    5、总结

    单调队列的应用仍有很多实例,这一不能一一道出。

       首先考虑问题需要的时间复杂度如果是o(n)的算法,单调队列是首选。

       其次要善于观察分析,发现题目中的单调性。决策的单调(合并果子),要求问题的特性(window),元素价值和其在原序列中位置的单调(志愿者),问题结果的单调(广告印刷)。

    二、单调队列在优化动态规划中的应用

        做动态规划时常常会见到形如这样的转移方程:

      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排序的话,则g(k)必然是不降的。

      这样,就引导我们使用一个单调队列来维护决策表。对于每一个状态f(x)来说,计算过程分为以下几步:

      1、 队首元素出队,直到队首元素在给定的范围中。

      2、 此时,队首元素就是状态f(x)的最优决策,

      3、计算g(x),并将其插入到单调队列的尾部,同时维持队列的单调性(不断地出队,直到队列单调为止)。

      重复上述步骤直到所有的函数值均被计算出来。不难看出这样的算法均摊时间复杂度是O(1)的。因此求解f(x)的时间复杂度从O(n^2)降到了O(n)。

    单调队列指一个队列中的所有的数符合单调性(单调增或单调减),在信息学竞赛的一些题目上应用,会减少时间复杂度

    单调队列的每个元素一般会存储两个值:

    1.在原数列中的位置(下标)

    2.该元素在动态规划中的状态值(价值)

    单调队列同时保证这两个值单调。

    下面看几个优化的实例:

    1、 烽火传递

    描述 Description  

        烽火台又称烽燧,是重要的防御设施,一般建在险要处或交通要道上。一旦有敌情发生,白天燃烧柴草,通过浓烟表达信息:夜晚燃烧干柴,以火光传递军情。在某 两座城市之间有n个烽火台,每个烽火台发出信号都有一定的代价。为了使情报准确的传递,在m个烽火台中至少要有一个发出信号。现输入n、m和每个烽火台发 出的信号的代价,请计算总共最少需要话费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确的传递!!!

    输入格式 Input Format

             第一行有两个数n,m分别表示n个烽火台,在m个烽火台中至少要有一个发出信号。

             第二行为n个数,表示每一个烽火台的代价。

    输出格式 Output Format     

            一个数,即最小代价。       

    样例

    5 3

    1 2 5 6 2

    4

    时间限制 Time Limitation    

            各个测试点1s

    注释 Hint      

            1<=n,m<=1,000,000

    分析

    要用动态规划的方法解决。我们可以写出这样的方程f[i]:=min{f[j]}+a[i](i-m<=j<i-1)(因为要保证i之前的3个中必须存在被点亮的烽火台)。单纯这样循环会造成超时。

    我们想到了用单调队列进行优化,由于随着i的循环,每次只有一个i进入决策区间也只有一个i出决策区间,由于每次选取决策区间中的最小值,所以维护一个单调递增序列,每次取出队首元素即可。

    为什么可以将队尾元素无情的删去呢?由于后进队的序列同时满足在原序列中的位置更靠后和其在动态规划中的价值更大。这样选取这个元素就要比选取之前的任何一个决策要优,所以之前被删掉的决策都是无用的。

    这道题的本质就是用单调队列维护了决策本身的价值和其在原序列中位置的同时单调。

    要特别注意单调队列中的值是决策在原决策序列中的位置。

    2、难题的解决

    给定一个序列,读入n,k和a1~an,让你求出该序列中包含第k项的最长上升子序列长度。

       1<=n<=300000    k<=n

    分析

    不要被问题的表面现象迷惑,既然要包含第k项,那么在k项的左边,比第k项大

    的元素一定不会进入目标序列。同理在k项的右边,比第k项小的元素一定也不会进入目标队列。

    那么我们直接把这些无用的元素删去。

    剩下的队列就好看得多。在k项之前的元素都比k小,之后的都比k项大。我们只需从1~k-1求一遍最长上升子序列,再从k+1~n求一遍最长上升子序列即可。最后结果为二者的值加1。

    由于数据范围很大,用传统的n2算法无法完成任务。所以本题地关键是(nlogn)的lis

    假如x<y<t且a[x]<a[y],f[x]=f[y],也就是说选取x,y都可以更新得到相同的的f[t]的值,那么选取谁 呢。显然选取x会更优,因为在a[x]…a[y]中如果存在a[z],使得a[x]<a[z]<a[y]那么就可以通过x得到更长的最长不降 子序列的值。

    根据f的值进行分类,我们只需要保留满足f[t]=k的所有a[t]中的最小值,设d[k]记录这个值,即d[k]:=min{a[t]}(f[t]=k)

    我们注意到d有两个特点:

    1.       d[k]的值在整个过程中是单调不上升的。

    2.       d数组是有序的。即d1<d2<….<dn

    利用d,我们可以得到一种计算最长上升子序列的方法。设当前已经求出的最长上升序

    列的长度为len,每次读入一个新元素x。

    如果x>d[len]将其直接假如d,inc(len)得到更长的序列

    否则,在d中查找到第一个比该元素小的元素d[k],将该元素加入,d[k+1]:=x;如果使用

    顺序查找效率会很低,所以采用二分查找,将其复杂度降为log级,难点在于二分查找。

     本题中的单调队列的出现时利用决策的性质,用元素在动归中的价值分类。在入队操做

    时并未让所有元素出队,而是直接插入相应位置,这是根据题目的特殊性决定的。对于一个单调的序列往往用二分法。

    当然这种发法也可推广到其他的最XX序列问题

    上面说的已经很详细,代码就省了。

  • 相关阅读:
    python 赋值操作的知识点
    python while循环语句
    python dict遍历
    python列表的切片操作
    Python做下载器需要掌握哪些
    BeautifulSoup已经安装,但仍提示No module named
    python 列表循环输出中文
    python 字符串split (string split)
    python 调用解释器
    分享python字符串去除空格的知识点
  • 原文地址:https://www.cnblogs.com/zhaoxinshanwei/p/3539029.html
Copyright © 2011-2022 走看看