区间最值——单调队列の板子

队列是啥都知道对吧?

单调队列就是一个保证队列内元素单调的队列,这个队列和普通队列在操作上最大的不同就是既可以从队头弹出元素也可以从队尾弹出元素。队尾弹出是为了保证队列的单调性。

为啥我要让队列单调?

单调队列的基本用途就是求固定长度的区间的最值。。啊我不是说像rmq问题那样随便给个区间让求最值啥的。。它可能要求一系列区间,而这些区间的左端点和右端点排起序来都是单调的,那么用单调队列扫一遍就可以 O(n) 出解。其实有一些时候也不需要真的开出一个队列来啦。。平常用得很多的那种两个指针扫一遍的方法实际上跟单调队列就是差不多的啦。
举个例子来说,比如有一个DP方程是 f[i]=min{f[j]+delta(ik<=j<=i)} ,直接写出来就是个 n2 的,但是这个玩意儿就可以用单调队列优化到 O(n) 的。
还有就是单调队列这玩意儿可以用在很多题的乱搞上。。最重要的一个应用就是DP的斜率优化很多都用单调队列维护那个单调的斜率啦。

单调队列咋写啊?

还是维护head和tail指针,每次一个一个把要入队的元素往队列里加。以维护单调不降的队列为例,因为要求单调不降,所以如果要入队的元素v小于队尾元素q[tail],就要把队尾元素出队,重复操作直到q[tail]小于等于v或者队空,然后把v入队。这样的话队头元素每次维护的就是当前区间内的最小值。
哎呀这样干巴巴的说感觉也说不明白= =ATP并不相信自己的语文水平_ (:з」∠)_
还是贴一个例题吧:POJ-2823 Sliding Window
这个题的意思是给出一个序列n和区间长度k,每个长度为k的区间都会有一个区间最小值和区间最大值,求这些最小值和最大值。。这是一个单调队列的经典模型。
先贴代码再解释。。。

#include
#include
#include
using namespace std;
int n,k,a[1000010],head,tail;
struct queue{
    int v,inx;
    bool operator < (const queue &x) const{
      if (v<x.v) return 1;
      if (v>x.v) return 0;
      if (inx>x.inx) return 1;
      return 0;
    }
}q[1000010];
int comp(queue a,queue b)
{return bpush(int i)
{
    ++tail;
    q[tail].v=a[i];q[tail].inx=i;
}
int main()
{
    scanf("%d%d",&n,&k);
    for (int i=1;i<=n;i++)
      scanf("%d",&a[i]);
    memset(q,0,sizeof(q));
    head=tail=0;
    for (int i=1;i<=k;i++)
      push(i);
    sort(q+1,q+k+1);
    printf("%d ",q[head+1].v);
    for (int i=k+1;i<=n;i++)
    {
        while ((head!=tail)&&(q[head+1].inx1))
          ++head;
        while ((tail!=head)&&(q[tail].v>a[i]))
          --tail;
        push(i);printf("%d ",q[head+1].v);
    }
    memset(q,0,sizeof(q));
    head=tail=0;
    for (int i=1;i<=k;i++)
      push(i);
    sort(q+1,q+k+1,comp);
    printf("\n%d ",q[head+1].v);
    for (int i=k+1;i<=n;i++)
    {
        while ((q[head+1].inx1)&&(head!=tail))
          ++head;//进行这一步操作的时候一定要判一下队列是否为空再删除啊!
        while ((q[tail].vpush(i);printf("%d ",q[head+1].v);
    }
    printf("\n");return 0;
}

这个题可以用单调队列来维护一个长度为k的区间内的最值。以维护最小值为例,我们维护单调不降的队列,因为如果后面某个可以达到的地方出现了一个最小值,那么这个最小值会一直产生贡献直到它后面出现一个更小的值把它踢出去,于是这个最小值前面那些值都不会再产生贡献了,可以把它们扔出去。
首先把前k个先加进去(ATP这里用的方法太愚蠢了,还排了个序。。实际上每次加的时候也维护队列单调不降就可以了。并且队列里面也没有必要真正把值记录进去,只要记录元素编号然后通过元素编号来访问就可以了)。然后每次新向后扩展一个元素a[i],如果a[i]比队尾元素小就一直把队尾元素出队。就像上面说的那样,当a[i]存在的时候,这个队尾元素已经不会产生贡献了。
那么这样处理以后队头保存的一定是当前区间里的最小值。但是还要注意的一点是因为维护的区间长度是固定的,所以相当于左端点也是一直往右边移动的,所以在把队头元素作为答案之前还要判断一下它在不在当前区间里面,如果不在的话要一直出队。
这样的话可以发现在单调队列里,元素的位置和大小都是单调的。每个元素只会进队一次出队一次,均摊复杂度是 O(n) 的。
并且,单调队列里面的东西因为满足单调性,所以可以在里面二分。。

有没有别的题目来做啊?

这里贴几个题目。。ATP做的单调队列的题目其实比较少qwq。。

  • CodeVS-4654 修剪草坪
    单调队列优化DP的经典应用

  • BZOJ-1293 生日礼物
    并不算严格的单调队列。。但是这种两个指针来维护的思想和单调队列是一样的

  • BZOJ-2276 Temperature
    ATP把这题写愚蠢了,如果仔细想一想实际上还是很好写的,关键是找到题目里的某些大小关系。。。
  • BZOJ-1047 理想的正方形
    用来练习单调队列是很好的一道题。只要写的时候理清楚思路就很简单。。。然而搞笑的是这题题目里面数据范围是1000,然后CodeVS上莫名其妙出现了一组大小为1500的数据,让第一次交这题的所有人炸飞,还恶意满满地把这组数据起名叫killer!
  • BZOJ-2096 Pilots
    也是比较显然的单调队列思路,注意边界条件的判断。

你可能感兴趣的:(板子们)