单调队列优化背包

单调队列优化背包

单调队列:

  • 概念:单调队列是一个双端队列,内部元素具有单调性(单调增或单调减),且保持先插入的一定在后插入的前面(也就是维护第二个值——在原序列中的编号,是递增的);

    有两个操作:

    1. 插入:从对尾插入,和目前对尾作比较,如果会破坏单调性就删除,直到找到不会破坏单调性的位置为止;
    2. 获取:直接从对头获取就OK,如果对头的序号(插入时所维护的第二个值,也就是在一个序列中的编号,再单调队列里是递增的)不满足要求就删除找下一个;
  • 用途:

    1. 用来求一段固定区间的最大值或最小值,例如滑动窗口就是道很典型的题;

    2. 优化有关固定区间长度转移的DP,例如转移方程形如:

      \[dp[i]=min/max\{dp[j]\},(j\in(i-m,i)) \]

      可以使用单调队列来优化;

    3. 优化多重背包,这也是这篇博客的主题;


单调队列优化多重背包

(下面都是我的个人观点,个人认为比较通俗易懂,适合像我这样的蒟蒻食用(逃))

定义\(f[i][j]\)为前\(i\)个物品使用容量\(j\)所能获得的最大价值;

对于一个容量为\(C\)的背包,对于\(d\)个价值为$ w\(,体积为\)v$ 物品;

那么对于\(f[i][j]\)它只能从\(f[i-1][j-k*v](k\leq d\ and\ k*v\leq j)\)转移过来;

对于任何一个\(j\) ,都有\(j=a*v+b\) ,那么\(f[i-1][j-k*v]\Rightarrow f[i-1][(a-k)*v+b]\) ,由此可以看出我们可以把\(f[i][j]\)的转移由不同的余数\(b\)分为\(v\)类,这里给出一个直观的状态转移方程(\(b\)可以把它当做一个常数):

\[f[i][a*v+b]=max\{f[i-1][(a-k)*v_i+b]+k*w_i\} \]

(ps:由此往上你都可以通过画图(我是把它想像成了一个能量条来理解(滑稽)) 来感性理解,下面的就不太好感性理解了,我曾试图感性理解,但发现很容易就会想偏,所以不太推荐感性理解

手动变形一下:(ps:这里\(a\)还是原来的意思,然后用\(k\)代替了\((a-k)\))

\[f[i][j]=max\{f[i-1][k*v_i+b]+(a-k)*w_i\}\quad (k\leq a\ and\ (a-k)\leq d) \]

这里可以把\(a*w_i\)提出来,这样就成了你平常见到的单调队列优化的方程了:

\[f[i][j]=max\{f[i-1][k*v_i+b]+k*w_i\}+a*w_i\quad(k\leq a \ and\ (a-k)\leq d) \]

你看,这样是不是就很类似上面的单调队列的用途\(2\)的DP方程了;

根据这个方程,我们就可以用单调队列了,找\(f[i-1][k*v_i+b]+k*w_i\)在右端点为\(a\)长度为\(d\)的区间中的最大值(这里把\(f[i-1][k*v_i+b]+k*w_i\)看成序列中的元素,\(a,k\)都为序列的下标);

这里\(f[][]\)可以用滚动数组降一维;(这个大家应该都会)

如果还有什么不懂的地方,可以结合代码来加深理解;

code

(原题链接)

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e4+5;
int f[N];
int n,m,C;
int q[N],val[N];
inline int read()
{
    int f=1;
    char ch;
    while((ch=getchar())<'0'||ch>'9') if(ch=='-') f=-1;
    int res=ch-'0';
    while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0';
    return res*f;
}
inline void solve(int v,int w,int d)//单调队列优化。
{
    for(int b=0;b<v;b++)
    {
        int k=0,head=1,tail=0;
        for(int j=b;j<=C;j+=v,k++)
        {
            int tmp=f[j]-k*w;
            while(head<=tail&&tmp>val[tail]) tail--;//滚动数组,所以要先插入。
            q[++tail]=k,val[tail]=tmp;
            while(head<=tail&&k-q[head]>d) head++;
            f[j]=val[head]+k*w;
        }
    }
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("travel.in","r",stdin);
    freopen("travel.out","w",stdout);
#endif
    n=read(),m=read(),C=read();
    for(int i=1,v,w,d;i<=n;i++)
    {
        v=read(),w=read(),d=read();
        solve(v,w,d);
    }
    for(int i=1,a,b,c;i<=m;i++)
    {
        a=read(),b=read(),c=read();
        for(int j=C;j>=0;j--)
            for(int k=0;k<=j;k++)
                f[j]=max(f[j],f[j-k]+k*k*a+k*b+c);
    }
    printf("%d",f[C]);
    return 0;
}

对于第二种情况,直接\(c^2\)暴力枚举就好了,千万不要考虑二次函数的优化,数据有锅,越优化越慢,不信你去题解里找到那篇优化了的交一下,T飞。(不知道是不是我背)

这道题还考你卡常的技巧,卡卡常就过了,还要看评测机给不给力,运气好的话,一遍就过了,如果不行就多交几遍(本人亲身经历)

对于这道题,你还可以试试二进制拆分,不过据说会T两个点(卡卡常说不定就过了)。

posted @ 2020-10-07 15:40  zfz04  阅读(141)  评论(0编辑  收藏  举报