单调队列优化动态规划

单调队列优化动态规划专题

什么是单调(双端)队列

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

【单调队列的性质】

一般,在动态规划的过程中,单调队列中每个元素一般存储的是两个值:
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>
#include <cstdio>
using namespace std;

int n, m;
long long s[300005];
// 前缀和

list<int> queue;
// 链表做单调队列

int main() {
    cin >> n >> m;
    s[0] = 0;
    for (int i=1; i<=n; i++) {
        cin >> s[i];
        s[i] += s[i-1];
    }
    long long maxx = 0;
    for (int i=1; i<=n; i++) {
        while (!queue.empty() and s[queue.front()] > s[i])
            queue.pop_front();
        // 保持单调性
        queue.push_front(i);
        // 插入当前数据
        while (!queue.empty() and i-m > queue.back())
            queue.pop_back();
        // 维护区间大小,使i-m >= queue.back()
        if (i > 1)
            maxx = max(maxx, s[i] - s[queue.back()]);
        else
            maxx = max(maxx, s[i]);
        // 更新最值
    }
    cout << maxx << endl;
    return 0;
}

【Vijos1243】生产产品

【在线测试提交传送门】

时间限制:1S / 空间限制:256MB

【问题描述】

  产品的生产需要M个步骤,每一个步骤都可以在N台机器中的任何一台完成,但生产的步骤必须严格按顺序执行。由于这N台机器的性能不同,它们完成每一个步骤的所需时间也不同。机器i完成第j个步骤的时间为T[i,j]。把半成品从一台机器上搬到另一台机器上也需要一定的时间K。同时,为了保证安全和产品的质量,每台机器最多只能连续完成产品的L个步骤。也就是说,如果有一台机器连续完成了产品的L个步骤,下一个步骤就必须换一台机器来完成。
  请计算最短需要多长时间。

【输入格式】

第一行有四个整数M, N, K, L;
接下来N行,每行有M个整数。第I+1行的第J个整数为T[J,I]。

【输出格式】

输出只有一行,表示需要的最短时间。

【输入样例1】

3 2 0 2
2 2 3
1 3 1

【输出样例1】

4

【数据范围】

对于50%的数据,N≤5,L≤4,M≤10000
对于100%的数据,N≤5, L≤50000,M≤100000

【解题思路】

转移方程为: f[i][j]=min( f[t][p]+sum[j][i]-sum[p][j]) 化简后可以得到 f[i][j]=min( f[t][p]-sum[p][j])+sum[j][i] 对于每一个j考虑开一个单调队列优化 ,维护 t和f[t][p]-sum[p][j]单调递增。这样每次从队首取出符合要求的一个即可更新.
q[a][x][0]表示队列中这个位置的的t q[a][x][1]表示这个位置的p
每次先更新所有的f[i][j]然后再更新所有的队列q[k] 
#include <bits/stdc++.h> 
using namespace std;
const int N=100010,INF=2099999999;
int q[10][N][2],l[10],r[10];
int m,n,cost,L,sum[10][N],f[N][10],ans=INF;
int main(){
  scanf("%d%d%d%d",&m,&n,&cost,&L);
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++) scanf("%d",&sum[i][j]),sum[i][j]+=sum[i][j-1];
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++) f[j][i]=INF;
  for(int i=1;i<=n;i++) q[i][ r[i]++ ][0]=0;

  for(int i=1;i<=m;i++){
    for(int k=1;k<=n;k++){
      while(l[k]<r[k] && i-q[k][ l[k] ][0]>L) l[k]++;
      int t=q[k][ l[k] ][0],p=q[k][ l[k] ][1];
      f[i][k]=min(f[i][k],f[t][p]+sum[k][i]-sum[k][t]+cost);
    }
    for(int k=1;k<=n;k++) 
      for(int j=1;j<=n;j++)//用f[i][k] 的值来更新 q[j];
        if(j!=k) {
          while(l[j]<r[j] && f[i][k]-sum[j][i]<= f[ q[j][r[j]-1][0] ][q[j][r[j]-1][1]] - sum[j][q[j][r[j]-1][0]]) r[j]--;
          q[j][r[j]][0]=i;q[j][r[j]++][1]=k;
        }
  }
  for(int i=1;i<=n;i++) ans=min(ans,f[m][i]);
  printf("%d",ans-cost);
  return 0;
}
  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值