动态规划之单调队列优化专题【附题目练习清单】

什么是单调(双端)队列

单调队列,顾名思义,就是一个元素单调的队列,那么就能保证队首的元素是最小(最大)的,从而满足动态规划的最优性问题的需求。
单调队列,又名双端队列。双端队列,就是说它不同于一般的队列只能在队首删除、队尾插入,它能够在队首、队尾同时进行删除。

【单调队列的性质】

一般,在动态规划的过程中,单调队列中每个元素一般存储的是两个值:
1.在原数列中的位置(下标)
2.他在动态规划中的状态值
而单调队列则保证这两个值同时单调。

【单调队列有什么用】

我们来看这样一个问题:一个含有n项的数列(n<=2000000),求出每一项前面的第m个数到它这个区间内的最小值。
这道题目,我们很容易想到线段树、或者st算法之类的RMQ问题的解法。但庞大的数据范围让这些对数级的算法没有生存的空间。我们先尝试用动态规划的方法。用代表第个数对应的答案,表示第个数,很容易写出状态转移方程:

这里写图片描述

这个方程,直接求解的复杂度是O(nm)的,甚至比线段树还差。这时候,单调队列就发挥了他的作用:
我们维护这样一个队列:队列中的每个元素有两个域{position,value},分别代表他在原队列中的位置和,我们随时保持这个队列中的元素两个域都单调递增。
那计算的时候,只要在队首不断删除,直到队首的position大于等于,那此时队首的value必定是的不二人选,因为队列是单调的!
我们看看怎样将 插入到队列中供别人决策:首先,要保证position单调递增,由于我们动态规划的过程总是由小到大(反之亦然),所以肯定在队尾插入。又因为要保证队列的value单调递增,所以将队尾元素不断删除,直到队尾元素小于。

【时间效率分析】

很明显的一点,由于每个元素最多出队一次、进队一次,所以时间复杂度是O(n)。用单调队列完美的解决了这一题。

【为什么要这么做】

我们来分析为什么要这样在队尾插入:为什么前面那些比大的数就这样无情的被枪毙了?我们来反问自己:他们活着有什么意义?!由于是随着单调递增的,所以对于存在j<i,a[j]>a[i],在计算任意一个状态的时候,都不会比优,所以j被枪毙是“罪有应得”。
我们再来分析为什么能够在队首不断删除,一句话:是随着单调递增的!

【一些总结】

对于这样一类动态规划问题,我们可以运用单调队列来解决:

这里写图片描述

其中bound[x]随着x单调不降,而const[i]则是可以根据i在常数时间内确定的唯一的常数。这类问题,一般用单调队列在很优美的时间内解决。


【Jzoj1771】烽火传递

【在线测试提交传送门】

【问题描述】

  烽火台又称烽燧,是重要的军事防御设施,一般建在险要或交通要道上。一旦有敌情发生,白天燃烧柴草,定代价。
  为了使情报准确地传递,在连续m个烽火台中至少要有一个发出信号。请计算总共最少花费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确传递。

【输入格式】

 第一行:两个整数N,M。其中N表示烽火台的个数,M表示在连续m个烽火台中至少要有一个发出信号。
接下来N行,每行一个数Wi,表示第i个烽火台发出信号所需代价。 

【输出格式】

一行,表示答案。 

【输入样例1】

5 3 
1 
2 
5 
6 
2

【输出样例1】

4

【解题思路】

设f[i]表示i必须选时最小代价。 
初值: f[0]=0 f[1..n]=∞
方程: f[i]=min(f[j])+w[i] 并且max(0,i-m)≤j<i
为什么j有这样的范围?如果j能更小,那么j~i这段区间中将有不符合条件的子区间,就会错。应保证不能有缝隙。 
 最后在f[n-m+1..n]中取最小值即答案 , 时间复杂度O(nm)
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
int w[100001];
int que[100001],head=0,tail=0;
int f[100001];
int main()
{
    scanf("%d%d",&n,&m);
    int i,j;
    for (i=1;i<=n;++i)
        scanf("%d",&w[i]);
    memset(f,127,sizeof f);
    f[0]=0;
    que[0]=0;
    for (i=1;i<=n;++i)
    {
        if (que[head]<i-m)
            ++head;//将超出范围的队头删掉
        f[i]=f[que[head]]+w[i];//转移(用队头)
        while (head<=tail && f[que[tail]]>f[i])
            --tail;//将不比它优的全部删掉
        que[++tail]=i;//将它加进队尾
    }
    int ans=0x7f7f7f7f;
    for (i=n-m+1;i<=n;++i)
        ans=min(ans,f[i]);
    printf("%d\n",ans);
}

【Tyvj1305】最大最大子序和

【在线测试提交传送门】

【问题描述】

输入一个长度为n的整数序列,从中找出一段不超过M的连续子序列,使得整个序列的和最大。
例如 1,-3,5,1,-2,3
当m=4时,S=5+1-2+3=7;
当m=2或m=3时,S=5+1=6。

【输入格式】

第一行两个数n,m;
第二行有n个数,要求在n个数找到最大子序和。

【输出格式】

一个数,数出他们的最大子序和。

【输入样例】

6 4
1 -3 5 1 -2 3

【输出样例】

7

【数据范围】

n,m≤300000;数列元素的绝对值≤1000。

【解题思路】

这是一个典型的动态规划题目,不难得出一个1D/1D方程:
  f(i) = sum[i]-min{sum[k]|i-M≤k≤i} 

  由于方程是1D/1D的,所以我们不想只得出简单的Θ(n^2)算法。不难发现,此优化的难点是计算min{sum[i-M]..sum[i-1]}。在上面的链接中,我们成功的用Θ(nlgn)的算法解决了这个问题。但如果数据范围进一步扩大,运用st表解决就力不从心了。所以我们需要一种更高效的方法,即可以在Θ(n)的摊还时间内解决问题的单调队列。

  单调队列(Monotone queue)是一种特殊的优先队列,提供了两个操作:插入,查询最小值(最大值)。它的特殊之处在于它插入的不是值,而是一个指针(key)(wiki原文:imposes the restriction that a key (item) may only be inserted if its priority is greater than that of the last key extracted from the queue)。所谓单调,指当一组数据的指针1..n(优先级为A1..An)插入单调队列Q时,队列中的指针是单调递增的,队列中指针的优先级也是单调的。因为这里要维护优先级的最小值,那么队列是单调减的,也说队列是单调减的。

查询最小值
由于优先级是单调减的,所以最小值一定是队尾元素。直接取队尾即可。
插入操作:
当一个数据指针i(优先级为Ai)插入单调队列Q时,方法如下:
1.如果队列已空或队头的优先级比Ai大,删除队头元素。
2.否则将i插入队头

比如说,一个优先队列已经有优先级分别为 {5,3,-2} 的三个元素,插入一个新元素,优先级为2,操作如下:
1.因为2 < 5,删除队头,{3,-2}
2.因为2 < 3,删除队头,{-2}
3.因为2 > -2,插入队头,{2,-2}

证明性质可以得到维护

  证明指针的单调减 :由于插入指针i一定比已经在队列中所有元素大,所以指针是单调减的。 
证明优先级的单调减:由于每次将优先级比Ai大的删除,只要原队列优先级是单调的,新队列一定是单调的。用循环不变式易证正确性。 
  为什么删除队头:直观的,指针比i小(靠左)而优先级比Ai大的数据没有希望成为任何一个需要的子序列中的最小值。这一点是我们使用优先队列的根本原因。

维护区间大小

当一串数据A1..Ak插入时,得到的最小值是A1..Ak的最小值。反观dp方程:

f(i) = sum[i]-min{sum[k]|i-M≤k≤i} 1

在这里,A = sum。对于f(i),我们需要的其实是Ai-M .. Ai的最小值,而不是所有已插入数据的最小值(A1..Ai-1)。所以必须维护区间大小,使队列中的元素严格处于Ai-M..Ai-1这一区间,或者说删去哪些A中过于靠前而违反题目条件的值。由于队列中指针是单调的,也就是靠左的指针大于靠右的,或者说在优先队列中靠左的值,在A中一定靠后;优先队列中靠右的值,在A中一定靠前。我们想要删除过于靠前的,只需要在优先队列中从右一直删除,直到最右边(队尾)的值符合条件。具体地:当队头指针p满足i-m≤p时。 
 形象地说,就是忍痛割爱删去哪些较好但是不符合题目限制的数据。
#include <iostream>
#include <list></
  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值