动态规划的单调队列优化(含多重背包)

来源:互联网 发布:5s蜂窝移动数据设置 编辑:程序博客网 时间:2024/05/16 06:08

什么是单调队列

单调队列就是元素单调的队列,譬如一个队列中的元素为1,2,3,4,5,6,单调递增,这就是一个单调队列。咱们先看一道单调队列的模板题:poj2823/洛谷P1886
怎么维护单调队列呢?譬如维护一个单调递增的队列,就是要进入一个元素的时候,把队尾小于它的元素统统出队即可。而在例题中,我们还要记录每个元素在原来数组中的下标以确定是否可用,如果已经出了当前窗口,则出队。
代码:

void getmin(){//单调递增    int he=1,ta=1,i;    for(i=1;i<=n;i++){        while(he<ta&&q[ta-1]>=a[i])ta--;        q[ta]=a[i];bj[ta]=i;ta++;        if(i>=m){            while(he<ta&&bj[he]<=i-m)he++;            printf("%d ",q[he]);        }    }}void getmax(){//单调递减    int he=1,ta=1,i;    for(i=1;i<=n;i++){        while(he<ta&&q[ta-1]<=a[i])ta--;        q[ta]=a[i];bj[ta]=i;ta++;        if(i>=m){            while(he<ta&&bj[he]<=i-m)he++;            printf("%d ",q[he]);        }    }}

单调队列优化动态规划

例题1:洛谷P1725 琪露诺

链接:走你╭(′▽`)╯
这题就当是单调队列入门啦。
大家都知道f[i]=max(f[j])+v[i](ir<=j<=il)
直接这么dp肯定超时,那么我们可以把f[ir]f[il]这一段都扔到单调队列里,然后取队首即可
单调队列一定不能删除还有可能用到的元素,也不能添加暂时不会用的元素,所以我们要确保在用单调队列时,il加了进去,没有被其后的元素删掉,而其后的东西也没有加进去。
所以就有了代码中的写法
代码:

#include<iostream>#include<cstdio>#include<algorithm>#include<cstring>#include<map>#include<cmath>using namespace std;#define ll long longll read(){    ll q=0,w=1;char ch=' ';    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();    if(ch=='-')w=-1,ch=getchar();    while(ch>='0'&&ch<='9')q=q*10+(ll)(ch-'0'),ch=getchar();    return q*w;}const int maxn=200005;int n,l,r;ll v[maxn],f[maxn],ans;int bj[maxn];int main(){    int i,j,ta=1,he=1;ll kl;    n=read();l=read();r=read();    for(i=0;i<=n;i++)v[i]=read();    f[0]=v[0];    for(i=l;i<=n;i++){        while(he<ta&&f[bj[ta-1]]<=f[i-l])ta--;        bj[ta]=i-l;ta++;        while(he<ta&&bj[he]<i-r)he++;        f[i]=f[bj[he]]+v[i];        if(i>=n-r)ans=max(ans,f[i]);    }    printf("%lld",ans);    return 0;}

例题2:UESSTC594我要长高

链接:走你╭(′▽`)╯
这题充满了恶意啊……
容易想到用f[i][j]表示第i个儿子长j这么高的时候的最小损失值(x[i]表示i儿子本来的身高,则:

f[i][j]=min(f[i1][k]+abs(jk)c+(x[i]j)(x[i]j))

现在我们分类讨论一下,假如j>k:
f[i][j]=min(f[i1][k]+(jk)c+(x[i]j)(x[i]j)

变形可得:
f[i][j]=min((f[i1][k]kc)+(jc+(x[i]j)(x[i]j)))

显然前面那一坨可以塞在一个单调队列里来求小于j的情况下的最优k,具体怎么实现看代码吧。然后j<k的情况也差不多:
f[i][j]=min((f[i1][k]+kc)+(jc+(x[i]j)(x[i]j)))

得到了美妙的代码:

#include<iostream>#include<cstdio>#include<climits>#include<algorithm>#include<cstring>#include<cmath>using namespace std;int read(){    int q=0;char ch=' ';    while(ch<'0'||ch>'9')ch=getchar();    while(ch>='0'&&ch<='9')q=q*10+ch-'0',ch=getchar();    return q;}const int maxn=105;int n,c,inf=0x3f3f3f3f;int f[2][maxn],q[maxn];int main(){    int x,i,j,t,ans,he,ta;    while(scanf("%d%d",&n,&c)==2){        x=read();t=1;        for(i=0;i<x;i++)f[t][i]=inf;        for(i=x;i<=100;i++)f[t][i]=(x-i)*(x-i);        for(i=2;i<=n;i++){            t=i&1;x=read();            he=ta=1;            for(j=0;j<=100;j++){//比前一个人高,显然弄到j的时候k取0~j-1的情况都已讨论过                int kl=f[t^1][j]-j*c;                while(he<ta&&q[ta-1]>=kl)ta--;                q[ta]=kl;ta++;                if(j<x)f[t][j]=inf;                else f[t][j]=q[he]+j*c+(x-j)*(x-j);            }            he=ta=1;            for(j=100;j>=0;j--){//比前一个人矮,显然弄到j的时候k取j+1~100的情况都已讨论过                int kl=f[t^1][j]+j*c;                while(he<ta&&q[ta-1]>=kl)ta--;                q[ta]=kl;ta++;                if(j<x)f[t][j]=inf;                else f[t][j]=min(f[t][j],q[he]-j*c+(x-j)*(x-j));            }        }        t=n&1;ans=inf;        for(i=0;i<=100;i++)ans=min(ans,f[t][i]);        printf("%d\n",ans);    }    return 0;}

例题3:HDU3401

链接:走你╭(′▽`)╯
题目大意是买股票,第i天买花ap[i]元,卖得bp[i]元,可以买as[i]张或者卖bs[i]张,两次交易之间必须间隔w天,并且手上最多持有m股,求最多赚多少钱。
f[i][j]表示第i天持有j股的最多收益,
如果不交易,那么f[i][j]=f[i1][j]
如果买f[i][j]=f[iw1][k](jk)ap[i]
如果卖f[i][j]=f[iw1][k]+(kj)bp[i]
(因为不交易的状态已经转移了,所以买和卖只要考虑第iw1天即可)
然后我们把状态转移方程变形一下,就是
买:f[i][j]=(f[iw1][k]+kap[i])jap[i]
卖:f[i][j]=(f[iw1][k]+kab[i])jbp[i]
前面那陀塞单调队列里,处理一下边界,然后就是考虑一下买的数量的问题即可。

#include<iostream>#include<cstdio>#include<climits>#include<algorithm>#include<cstring>#include<cmath>using namespace std;int read(){    int q=0;char ch=' ';    while(ch<'0'||ch>'9')ch=getchar();    while(ch>='0'&&ch<='9')q=q*10+ch-'0',ch=getchar();    return q;}const int N=2005;int T,n,m,w,ans,inf=0xfffffff;int ap[N],bp[N],as[N],bs[N],f[N][N],q[N],bj[N];int main(){    int i,j,he,ta,kl;    T=read();    while(T--){        n=read();m=read();w=read();        for(i=1;i<=n;i++)            ap[i]=read(),bp[i]=read(),as[i]=read(),bs[i]=read();        for(i=0;i<=n;i++)            for(j=0;j<=m;j++)f[i][j]=-inf;        for(i=1;i<=w+1;i++)            for(j=0;j<=m&&j<=as[i];j++)f[i][j]=-j*ap[i];        for(i=1;i<=n;i++){            for(j=0;j<=m;j++)f[i][j]=max(f[i][j],f[i-1][j]);            if(i<=w+1)continue;            he=ta=1;            for(j=0;j<=m;j++){//买                kl=f[i-w-1][j]+j*ap[i];                while(he<ta&&q[ta-1]<=kl)ta--;                q[ta]=kl;bj[ta]=j;ta++;                while(he<ta&&j-bj[he]>as[i])he++;                f[i][j]=max(f[i][j],q[he]-j*ap[i]);            }            he=ta=1;            for(j=m;j>=0;j--){//不买                kl=f[i-w-1][j]+j*bp[i];                while(he<ta&&q[ta-1]<=kl)ta--;                q[ta]=kl;bj[ta]=j;ta++;                while(he<ta&&bj[he]-j>bs[i])he++;                f[i][j]=max(f[i][j],q[he]-j*bp[i]);            }        }        ans=0;        for(i=0;i<=m;i++)ans=max(ans,f[n][i]);        printf("%d\n",ans);    }    return 0;}

例题4:POJ1821

链接:走你╭(′▽`)╯
题目大意:你带着一群工人刷墙,第i个工人被502胶粘在了s[i]号位子上,他由于手臂长度,唯一的刷墙方式是大手一挥,将第s[i]格加上两边的格子共计k个刷好(0<=k<=l[i],刷了就必须刷s[i]格),然后他刷一格要p[i]元的工资。你现在想尽可能多的坑钱,但是反复刷一个格子太明显了是要不得的,求最多可以坑多少钱。
设f[i][j]表示前i个工人刷j个格子(显然工人已经按照站位排好序了),那么:

f[i][j]=max(f[i][j1],f[i1][j],f[i1][k]+(jk)p[i])

分别表示第j面墙不刷,第i个工人自己玩儿去,和一个转移。
于是我们把后面的式子变形,有k的放在一块儿(肯定大家已经会变了吧,f[i][j]=(f[i1][k]kp[i])+jp[i]
不过边界值是很麻烦的,对于可以作为k的值,一定满足k<s[i],对于可以使用第3个方程的j值,一定满足j>=s[i]

#include<iostream>#include<cstdio>#include<climits>#include<algorithm>#include<cstring>#include<cmath>using namespace std;int read(){    int q=0;char ch=' ';    while(ch<'0'||ch>'9')ch=getchar();    while(ch>='0'&&ch<='9')q=q*10+ch-'0',ch=getchar();    return q;}int n,m,ans;struct node{int l,p,s;}t[105];int f[105][16005],q[16055],bj[16055];bool cmp(node a,node b){return a.s<b.s;}int main(){    int i,j,he,ta,kl;    m=read();n=read();    for(i=1;i<=n;i++)t[i].l=read(),t[i].p=read(),t[i].s=read();    sort(t+1,t+1+n,cmp);    for(i=1;i<=n;i++){        he=ta=1;        for(j=0;j<=m;j++){            if(j!=0)f[i][j]=max(f[i-1][j],f[i][j-1]);            else f[i][j]=f[i-1][j];            if(j>=t[i].s+t[i].l)continue;//有些木板不能涂色            if(j<t[i].s){//符合条件的才可以入队!                kl=f[i-1][j]-j*t[i].p;                while(he<ta&&q[ta-1]<=kl)ta--;                q[ta]=kl;bj[ta]=j;ta++;                continue;//第i个人不能涂这些木块            }            while(he<ta&&j-bj[he]>t[i].l)he++;            f[i][j]=max(f[i][j],q[he]+t[i].p*j);        }    }    for(i=1;i<=m;i++)ans=max(ans,f[n][i]);    printf("%d",ans);    return 0;}

多重背包单调队列优化

例题:HDU2191,codevs5429
优化的原理是啥呢?w[i]表示物品重量,v[i]表示价值,c[i]表示数量,我们知道朴素状态转移方程:
f[i][j]=max(f[i1][jw[i]k]+v[i]k);(k<=c[i])
现在我们要把这个方程变成一个单调队列可以优化的形式,于是我们假设d=jmodw[i],s=jw[i]
f[i][j]=max(f[i1][d+w[i]k]v[i]k)+v[i]s(s-k<=c[i])
这应该是没有问题的吧?所以我们枚举余数d,然后对于每个余数d都用单调队列优化即可。

#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<climits>using namespace std;int n,m,he,ta,T;int f[7005],q[7005],num[7005];int main(){    int i,j,w,v,s,d;    scanf("%d",&T);    while(T--){        scanf("%d%d",&m,&n);        for(i=0;i<=m;i++)f[i]=0;        for(i=1;i<=n;i++){            scanf("%d%d%d",&w,&v,&s);            if(s>m/w)s=m/w;            for(d=0;d<w;d++){                he=ta=1;                for(j=0;j<=(m-d)/w;j++){//先存进去,后取出来                    int tmp=f[j*w+d]-v*j;                    while(he<ta&&q[ta-1]<=tmp)--ta;                    q[ta]=tmp,num[ta++]=j;                    while(he<ta&&j-num[he]>s)++he;                    f[j*w+d]=max(f[j*w+d],q[he]+v*j);                }            }        }        printf("%d\n",f[m]);    }    return 0;}

总结

可以用单调队列优化的dp题在将方程变形后,有一段可以看做不含有当前状态j,只含有前置状态k的一个整体,这个整体可以塞到单调队列里。