【C++】单调队列优化动态规划

引入

可用单调队列优化的动规有一大类题型,它们多半都有一个特征:
可以化归为序列中定长区间的最值问题。注意这里必须是定长区间,否则应用RMQ算法。
下面举一个例子:

  • 输入:第一行两个正整数N(N<=600000),M。接下来一行N个数。

  • 输出:对于每个区间[i,i-M+1],输出其中的最小值

  • 思路:很显然这道题数据大到不允许利用RMQ的各种O(NlogN)的算法,想到每一次找最小值都只是将上一个区间后移一个数,即这个区间的答案很有可能可从上个区间获得,于是保持一个单调队列,区间每后移一次,就将num[i]插入队尾,若队尾的数Q[k]大于等于当前的数(num[i]),就说明Q[k]在以后就不可能是一个可行解,删去它,这样循环操作,直至Q[k]<num[i]为止。
    对于每一个区间的最小值,只需输出当前队最前面的且在num中的下标号大于i-M+1的值即可。这样复杂度就从O(NlogN)飞跃到了O(N)。
    此外,有很多问题可以化归为此问题进行解决,如求序列中长度不大于定值的最大连续子序列和等问题,并且此方法可以套用于满足上述性质的非动归题目,应用范围极广。

例题1 Diving the Path

POJ-2373
链接:http://poj.org/problem?id=2373
https://vjudge.net/problem/POJ-2373

在这里插入图片描述

题目大意

有一个长度为L(1000000)的区间,L是偶数,喷水的机器半径是a到b,
限制:1区间不能重复覆盖,2部分区域一定只能被一个机器覆盖。
求最少需要多少机器。

思路

f[i]表示长度为i的最少需要几个设备。
f[i]=1+min(f[i-2a]…f[i-2b]);
把f[i-2a]到f[i-2b]看成一个滑动窗口,维护一个从小到大的队列,左边可以出去,右边也可以出去,右边出去的条件是当前新加入的数小于等于队尾,就可以把队尾删除。

同时注意,由于某些区域只能由一个设备覆盖,所以对于[s,t]的这种区域,我们对于所有的s<i<t,都让f[i]=无穷大+1;
而且只有当i是偶数的时候,f[i]才有解。
q数组里面记录的是下标.

代码

#include <iostream>
#include <cstdio>

#define R          register int
#define re(i,a,b)  for(R i=a; i<=b; i++)
 
using namespace std;

int const inf=(int)1e7;
int const N=1000005;

int a,b,n,l;
int f[N],q[N];

int main() {
    scanf("%d%d%d%d",&n,&l,&a,&b);
    re(i,1,l) f[i]=inf;
    re(i,0,n-1) {
        int x,y;
        scanf("%d%d",&x,&y);
        re(j,x+1,y-1) f[j]=inf+1;
    }
    f[0]=0;
    int ft=0,lt=-1;
    for(int i=2; i<=l; i+=2) {
        while(ft<=lt && q[ft]<i-2*b) ft++;
        if(i-2*a>=0) {
            while(ft<=lt && f[q[lt]]>=f[i-2*a]) lt--;
            q[++lt]=i-2*a;
        }
        if(f[i]==inf+1) continue;
        if(ft<=lt) f[i]=1+f[q[ft]];
    }
    if(f[l]>=inf) f[l]=-1;
    printf("%d\n",f[l]);
    return 0;
}

例题2 最大子段和

最大子段和(最大子序和)
链接:
Luogu:https://www.luogu.com.cn/problem/P1115
51Nod:https://www.51nod.com/Challenge/Problem.html#problemId=1049
vjudge:https://vjudge.net/problem/51Nod-1049
在这里插入图片描述

思路

f[i]表示以i为结尾的最大子序和
f [ i ] = s u m [ i ] m i n ( s u m [ i 1 ] , s u m [ i 2 ] . . s u m [ i m ] ) f[i]=sum[i]-min(sum[i-1],sum[i-2]..sum[i-m])
维护一个单调队列即可。

代码

#include <stdio.h>
#include <algorithm>

using namespace std;

typedef long long ll;

int const N=50005;

ll n;
ll a[N],f[N];

int main() {
    scanf("%lld",&n);
    for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
    for(int i=1; i<=n; i++) if(f[i-1]>0) f[i]=f[i-1]+a[i];
        else f[i]=a[i];
    ll ans=0;
    for(int i=1; i<=n; i++) ans=max(f[i],ans);
    printf("%lld\n",ans);
    return 0;
}

例题3 绿色通道

CodeVS-3342(可惜CodeVS死了)

题目描述

《思远高考绿色通道》(Green Passage, GP)是唐山一中常用的练习册之一,其题量之大深受lsz等许多oiers的痛恨,其中又以数学绿色通道为最。2007年某月某日,soon-if (数学课代表),又一次宣布收这本作业,而lsz还一点也没有写……

高二数学《绿色通道》总共有n道题目要写(其实是抄),编号1…n,抄每道题所花时间不一样,抄第i题要花a[i]分钟。由于lsz还要准备NOIP,显然不能成天写绿色通道。lsz决定只用不超过t分钟时间抄这个,因此必然有空着的题。每道题要么不写,要么抄完,不能写一半。一段连续的空题称为一个空题段,它的长度就是所包含的题目数。这样应付自然会引起马老师的愤怒。马老师发怒的程度(简称发怒度)等于最长的空题段长度。

现在,lsz想知道他在这t分钟内写哪些题,才能够尽量降低马老师的发怒度。由于lsz很聪明,你只要告诉他发怒度的数值就可以了,不需输出方案。(快乐融化:那么lsz怎么不自己写程序?lsz:我还在抄别的科目的作业……)

输入描述

第一行为两个整数n,t,代表共有n道题目,t分钟时间。
以下一行,为n个整数,依次为a[1], a[2],… a[n],意义如上所述。

输出描述

仅一行,一个整数w,为最低的发怒度。

样例输入

17 11
6 4 5 2 5 3 4 5 2 3 4 5 2 3 6 3 5

样例输出

3

数据范围

60%数据 n<=2000
100%数据 0 < n <=50000,0 < a[i] <=3000,0< t<=100000000

思路

先二分枚举答案 枚举一个最大的空题区间mid,然后dp判断是否可以达到
F[i]表示第i题一定要做的情况下,前i题满足条件,最少需要花费的时间
F[i]=min(f[j])+a[i]; i-mid-1<=j<=i-1 单调队列

代码

CodeVS没了,代码也没了。

例题4

POJ-1276
链接:http://poj.org/problem?id=1276
https://vjudge.net/problem/POJ-1276

在这里插入图片描述

思路

用单调队列优化多重背包
多重背包状态转移方程:
f [ i ] [ j ] = m a x ( f [ i 1 ] [ j k w [ i ] ] + k v [ i ] ) , 0 < = k < = m i n ( n u m [ i ] , j / w [ i ] ) ; f[i][j]=max(f[i-1][j-k*w[i]]+k*v[i]),0<=k<=min(num[i],j/w[i]);
有三重循环
二进制可以优化一下
通过单调队列可以优化到O(nm)
求f[i][0…m]
设余数为r,我们来求解f[i][r+k*w[i]];
r=0:
f [ i ] [ 0 ] , f [ i ] [ w [ i ] ] , f [ i ] [ 2 w [ i ] ] , f [ i ] [ k w [ i ] ] f[i][0] ,f[i][w[i]],f[i][2*w[i]],f[i][k*w[i]]
r=1:
f [ i ] [ 1 ] , f [ i ] [ 1 + w [ i ] ] , f [ i ] [ 1 + 2 w [ i ] ] , . . f [ i ] [ 1 + k w [ i ] ] f[i][1],f[i][1+w[i]],f[i][1+2*w[i]],..f[i][1+k*w[i]]
f [ i ] [ r ] , f [ i ] [ r + w [ i ] ] , f [ i ] [ r + 2 w [ i ] ] . . . f [ i ] [ r + k w [ i ] ] f[i][r],f[i][r+w[i]],f[i][r+2*w[i]]...f[i][r+k*w[i]]
f [ i ] [ j ] = m a x ( f [ i 1 ] [ r ] + x v [ i ] , f [ i 1 ] [ r + w [ i ] ] + ( x 1 ) v [ i ] , f [ i 1 ] [ r + 2 w [ i ] ] + ( x 2 ) v [ i ] , . . . . f [ i 1 ] [ r + x w [ i ] ] + 0 v [ i ] ) ; f[i][j]=max(f[i-1][r]+x*v[i],f[i-1][r+w[i]]+(x- 1)*v[i],f[i-1][r+2*w[i]]+(x-2)*v[i],....f[i- 1][r+x*w[i]]+0*v[i]);
f [ i ] [ j ] = m a x ( f [ i 1 ] [ j + k w [ i ] ] + ( x k ) v [ i ] ) ; f[i][j]=max(f[i-1][j+k*w[i]]+(x-k)*v[i]);
f [ i ] [ j ] = m a x ( f [ i 1 ] [ j + k w [ i ] ] k v [ i ] ) + x v [ i ] ; f[i][j]=max(f[i-1][j+k*w[i]]-k*v[i])+x*v[i];
f [ i 1 ] [ j + k w [ i ] ] k v [ i ] f[i-1][j+k*w[i]]-k*v[i]放到单调队列里面
k x k k表示在x个里面少取了k个
0 < = k < = m i n ( x , n u m [ i ] ) 0<=k<=min(x,num[i])
j m o d w [ i ] = r j mod w[i]=r
j d i v w [ i ] = x j div w[i]=x

代码

#include <cstdio>
#include <cstring>

using namespace std;

int const N=13;
int const M=100005;

int n,m;
int f[M],a[N],b[N],q[M],v[M];

int main() {
    while(scanf("%d",&m)!=EOF) {
        scanf("%d",&n);
        for(int i=1; i<=n; i++) scanf("%d%d",&b[i],&a[i]);
        memset(f,0,sizeof(f));
        for(int i=1; i<=n; i++) for(int r=0; r<a[i]; r++) {
            int st=0,ed=-1,k=0;
            for(int j=r; j<=m; j+=a[i],k++) {
                while(st<=ed && q[st]<k-b[i]) st++;
                while(st<=ed && v[ed]<=f[j]-k*a[i]) ed--;
                q[++ed]=k,v[ed]=f[j]-k*a[i];
                f[j]=v[st]+k*a[i];
            }
        }
        printf("%d\n",f[m]);
    }
    return 0;
}

其他练习

发布了73 篇原创文章 · 获赞 94 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Ljnoit/article/details/104568861