单调队列优化多重背包(含构造问题)

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
#include
#include
#include
#include
#include
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的优化,可就要根据具体题目来了,这也是我这里不赘述的原因。
希望大家能够自己去钻研这种题型,最后,希望我的一点见解能对大家有所帮助,谢谢观看。

你可能感兴趣的:(优先队列,动态规划)