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