单调队列优化多重背包(含构造问题<POJ 1742 coin>)

来源:互联网 发布:xp取消网络凭证 编辑:程序博客网 时间:2024/05/29 19:23

1.前言:
注: 本文中,用v[ i ] 表示物体的价值,w[ i ]表示物体的代价,c[ i ]表示物体的数量上限。

多重背包问题应该是动态规划的基础内容吧,我们先回顾一下多重背包的公式:
dp[ i ][ j ]表示选择到第 i 件物品,总代价为 j 时所获得的最大价值总和。

那么有:dp[ i ][ j ] = max( dp[ i-1 ][ j - k*w[i] ] + k*v[i] ); (0 <= k <= c[ i ])
滚动优化后有: dp[ j ] = ( dp[ j - k*w[i] ] + k*v[i] ); ( j : maxn – > w[i] )

显然,我们要枚举 i、j、k 三维,时间复杂度为O( n^3 )。
这样的时间复杂度很多时候是不能满足题目所需的。
解决办法有两种:二进制优化 与 单调队列优化。
今天在这里,我们就来看看单调队列的优化,它能使时间复杂度降到O( n^2 )。


2.公式的推演:

下面我们来对多重背包问题的公式进行变形

dp[ i ][ j ] = max( dp[ i-1 ][ j - k*w[i] ] + k*v[i] );

我们令 j = a*w[i] + b
代入原式中: dp[ i ][ a*w[i]+b ] = max( dp[ i-1 ][ a*w[i]+b - k*w[i]] + k*v[i] );

合并同类项:dp[ i ][ a*w[i]+b ] = max( dp[ i-1 ][ (a-k)*w[i]+b ] + k*v[i] );

分离常项:dp[ i ][ a*w[i]+b ] = max( dp[ i-1 ][ (a-k)*w[i]+b ] ) + k*v[i];

最关键的一步: 令 a - k = t !
所以 k = a - t

代入原式中: dp[ i ][ a*w[i]+b ] = max( dp[ i-1 ][ t*w[i]+b ] ) + (a-t)*v[i];
拆开:dp[ i ][ a*w[i]+b ] = max( dp[ i-1 ][ t*w[i]+b ] ) + a*v[i] - t*v[i];

移项,得到最终式子:
dp[ i ][ a*w[i]+b ] - a*v[i] = max( dp[ i-1 ][ t*w[i]+b ] - t*v[i]) ;


3.优先队列的实现:
刚刚得到的式子: dp[ i ][ a*w[i]+b ] - a*v[i] = max( dp[ i-1 ][ t*w[i]+b ] - t*v[i]) ;
观察左右式子,就会发现一个很神奇的事情:左右两边的式子是相等的!!!
先把代码贴出来:

for(i = 1; i <= N ; i ++)  {      for(b = 0 ; b <= w[i]-1 ; b ++)      {         hd = 1; tl = 0;         for(a = 0 ; a*w[i]+y <= M ;a ++)         {            int now = dp[i-1][a*w[i]+b]-a*v[i];            while( (a-lk[hd])>c[i] && hd<=tl)hd++;            while( (dp[i-1][lk[tl]*a[i]+b]-lk[tl]*v[i])<=now && hd<=tl)tl--;            lk[++tl] = k;            dp[i][a*w[i]+b] = dp[i-1][lk[hd]*w[i]+b]-lk[hd]*v[i]+a*v[i];          }       }   }

我们一步一步的看( j = a*w[i] + b):

for(i = 1; i <= N ; i ++)
//枚举物体

for(b = 0 ; b <= w[i]-1 ; b ++)
//枚举剩余类(即余数)

for(a = 0 ; a*w[i]+y <= M ;a ++)
//枚举件数(即a的值)

while( (a-lk[hd])>c[i] && hd<=tl)hd++; ----------------------------------------“¥”
//如果从队首转移所需件数大于件数上限,弹出队首元素

关键一步:
while( (dp[i-1][lk[tl]*a[i]+b]-lk[tl]*v[i])<=now && hd<=tl)tl--;
//如果队尾元素对应值还小于当前对应值,弹出队尾元素,即弹到队尾大于当前元素对应值为止。

//这个操作有必要解释一下为什么。
//由于我们按照顺序添加,前面的元素一定件数较少,更容易出现件数不够的现象(见上“¥”操作)。
//而如果价值又较低,又会被先淘汰,那么这个元素就是没有意义的,要弹出。

dp[i][a*w[i]+b] = dp[i-1][lk[hd]*w[i]+b]-lk[hd]*v[i]+a*v[i];
//dp[ i ][ a*w[i]+b ] - a*v[i] = max( dp[ i-1 ][ t*w[i]+b ] - t*v[i]) ;
//我们的初始公式移项计算 dp[ i ][ a*w[i]+b ];


4.多重背包变式:能否构造问题
简单来说,就是给你n个物体,问在总代价小于等于 J 的情况下,可以构造出价值总和为1~K中的多少个值。
题目其实就是POJ 1742 coin
传送门:http://poj.org/problem?id=1742
这个问题其实相对于前面的问题反而还简单了。
我们构造一个队列,存放可转移元素。
如果数量不够,队首出队。
这样的话只要队列中有元素,就说明可以转移。
代码:

#include<cstdio>#include<cstdlib>#include<iostream>#include<algorithm>#include<cmath>#include<cstring>using namespace std;inline int gi(){    int date = 0 , m = 1; char ch = 0;    while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();    if(ch == '-'){m = -1; ch = getchar();}    while(ch<='9' && ch>='0'){        date = date * 10 + ch - '0';        ch = getchar();    }return date * m;} //特别说一下://此题作为楼教主的男人八题之一,数据卡的非常紧//只用单调队列优化到O(n^2)竟然还过不去//必须要用下面的两个加速(01背包加速、乱搞加速)才跑的过去。bool dp[100005];int ans,n,m;int lk[100005];int a[150],c[150];void solve_coin(){    for(int i = 1; i <= m ; i ++)dp[i] = 0;    dp[0] = true; ans = 0;    for(int i = 1; i <= n ; i ++)    {        if(c[i] == 1){                                 //只有一件:01背包问题加速            for(int s = m ; s >= a[i]; s -- )             if(!dp[s] && dp[s - a[i]])dp[s] = true;        }         else if(a[i]*c[i]>=m){                         //数量多到乱搞都可以转移,加速            for(int s = a[i]; s <= m ; s ++)             if(!dp[s] && dp[s - a[i]])dp[s] = true;        }        //单调队列优化部分:        else for(int gg = 0 ; gg <= a[i] - 1; gg ++) //枚举余数        {            int hd = 1,tl = 0;            for(int s = 0; s*a[i] + gg <= m ; s ++)            {                while(hd<=tl && s - lk[hd]>c[i])hd++;            //队首弹出不符合元素                if(dp[s*a[i]+gg])lk[++tl]=s;                     //入队                else if(hd<=tl)dp[s*a[i]+gg] = true;             //转移            }        }     }    return;}int main(){    freopen("coin.in","r",stdin);    while(1)    {        n = gi(); m = gi();           //n个物品,询问1~m        if(n==0 && m==0)break;        for(int i = 1; i <= n ; i ++)a[i] = gi();   //输入每个物体的价值        for(int i = 1; i <= n ; i ++)c[i] = gi();   //输入每个物体的数量上限        solve_coin();        for(int i = 1; i <= m ; i ++)         if(dp[i])ans ++;        printf("%d\n",ans);    }return 0;}

5.尾言:
通过对多重背包的优化,大家应该可以看到单调队列的功效。
但在最后,我要说的是:单调队列有时候也可以优化DP!!
根据我自己的经验,一般可以这么做的DP有两个特点:
1> 涉及 取max 或者 取min
2> 通过我们的化简,可以将DP式左右两边化成同一形式。
不过单调队列对DP的优化,可就要根据具体题目来了,这也是我这里不赘述的原因。
希望大家能够自己去钻研这种题型,最后,希望我的一点见解能对大家有所帮助,谢谢观看。

原创粉丝点击