Package
背包?是DP入门基础,是理解动态规划最形象的介质。
背包问题:现有一堆物品,给出它们相应的代价(W)、价值(V) 以及你的背包的最大容纳量(MAX_W)。接下来求,你能一次性带走的最大物品价值(MAX_V)
不过既然你都看到这篇博客了,说明你已经对DP有所了解。博主也不多做引导式 骑法 启发了,直接进入正题。
01背包
给上文的问题加上 每种物品为单件 的条件就是01背包问题了(0和1就是二进制中无和有的意思
初学者我自然要推荐二维打表法,过程中的思想显而易见。后期在理解后,我会推荐一种占空间更少的方案。
二维矩阵打表法
我们来看一题 版子题1 HDU Bone Collector
题意:骨收藏家有一个体积为v的大袋子,路上有很多骨头,不同的骨头有不同的价值和体积,给出每一块骨头的价值和体积,计算他能在路上捡出的最大总价值
样例中给出的数据:
代价(体积) | 5 | 4 | 3 | 2 | 1 |
---|---|---|---|---|---|
价值 | 1 | 2 | 3 | 4 | 5 |
对以上样例,个别对数据敏感的人可以几秒钟出答案。可是我们做算法的人讲究步骤性和明确性,对这种题应该有清晰的思路。
- 我们需要一个二维矩阵,每一纵列是一种代价量,每一横列是一种物品,如下:
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
None | 0 | 0 | 0 | 0 | 0 | 0 |
A(代价5,价值1) | - | - | - | - | - | - |
B(代价4,价值2) | - | - | - | - | - | - |
C(代价3,价值3) | - | - | - | - | - | - |
D(代价2,价值4) | - | - | - | - | - | - |
E(代价1,价值5) | - | - | - | - | - | - |
二维矩阵每一空格代表一个状态下(一定代价量和物品取舍情况)所能达到最大价值,所以应该都初始化为 0 ,这里为了视觉效果,写的是 -
。
同时我为了之后方便行事,多添了一行None行,也就是什么都不取。
- 我们先放第一个物品A(代价5,价值1)
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
None | 0 | 0 | 0 | 0 | 0 | 0 |
A(代价5,价值1) | 0 | 0 | 0 | 0 | 0 | 1 |
B(代价4,价值2) | - | - | - | - | - | - |
C(代价3,价值3) | - | - | - | - | - | - |
D(代价2,价值4) | - | - | - | - | - | - |
E(代价1,价值5) | - | - | - | - | - | - |
只有代价为5时才能放下A,但之后我们取舍物品时是在 之前得出的 6(即n+1)种代价状态中进行修改变化
只有拿了它,可以在原先同代价下取得更大的价值才取用它;否则沿用上一状态同代价量的最大值,也就是不取用新的物品
- 我们再拿第二个物品 B
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
None | 0 | 0 | 0 | 0 | 0 | 0 |
A(代价5,价值1) | 0 | 0 | 0 | 0 | 0 | 1 |
B(代价4,价值2) | 0 | 0 | 0 | 0 | 2 | 2 |
C(代价3,价值3) | - | - | - | - | - | - |
D(代价2,价值4) | - | - | - | - | - | - |
E(代价1,价值5) | - | - | - | - | - | - |
代价量为 0 ~ 3 时,放不下B,沿用上一行的状态;
代价量为 4 时,能放B。若取用B,只留下代价 0 供其他物品使用,观察上一行的代价量 0 的价值量,加上B的价值量就是 目前 取用B带来的价值量;将其与不取用B而直接沿用上一行的价值量做比较,两者取最大。
代价量为 5 时,能放B。若取用B,只留下代价 1 供其他物品使用,观察上一行的代价量 1 的价值量,加上B的价值量就是 目前 取用B带来的价值量;将其与不取用B而直接沿用上一行的价值量做比较,两者取最大。
从中我们提取出 状态转移公式,DP[ i ] [ j ] = max ( DP[ i - 1 ][ j - W( i ) ] + V( i ) , DP[ i - 1 ][ j ] )
i 是代表第几个物品,j 是代表某个代价量, W(i) 代表物品 i 的代价 , V(i)代表物品 i 的价值(如果看懂了,也就知道我为什么多添了 None 行
- 第三个物品
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
None | 0 | 0 | 0 | 0 | 0 | 0 |
A(代价5,价值1) | 0 | 0 | 0 | 0 | 0 | 1 |
B(代价4,价值2) | 0 | 0 | 0 | 0 | 2 | 2 |
C(代价3,价值3) | 0 | 0 | 0 | 3 | 3 | 3 |
D(代价2,价值4) | - | - | - | - | - | - |
E(代价1,价值5) | - | - | - | - | - | - |
这个物品放置没什么特色,过( ̄︶ ̄)↗
- 第四个物品 D
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
None | 0 | 0 | 0 | 0 | 0 | 0 |
A(代价5,价值1) | 0 | 0 | 0 | 0 | 0 | 1 |
B(代价4,价值2) | 0 | 0 | 0 | 0 | 2 | 2 |
C(代价3,价值3) | 0 | 0 | 0 | 3 | 3 | 3 |
D(代价2,价值4) | 0 | 0 | 4 | 4 | 4 | 7 |
E(代价1,价值5) | - | - | - | - | - | - |
D行代价量为 5 时
取用D:观察上一行(C行)代价量为 3 的价值量,D价值加上C价值 ,第一个选项就出来了 7
不取用D:看上一行(C行)代价量为 5 的价值量,第二个选项就出来了 3
7 > 3,D行代价5 填 7
- 第五个物品 E
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
None | 0 | 0 | 0 | 0 | 0 | 0 |
A(代价5,价值1) | 0 | 0 | 0 | 0 | 0 | 1 |
B(代价4,价值2) | 0 | 0 | 0 | 0 | 2 | 2 |
C(代价3,价值3) | 0 | 0 | 0 | 3 | 3 | 3 |
D(代价2,价值4) | 0 | 0 | 4 | 4 | 4 | 7 |
E(代价1,价值5) | 0 | 5 | 5 | 9 | 9 | 9 |
根据状态转移公式,DP[ i ] [ j ] = max ( DP[ i - 1 ][ j - W( i ) ] + V( i ) , DP[ i - 1 ][ j ] )
我们会发现这公式会导致表格中,右边的值一定比左边大,下面的值一定比上面大
所以最后我们只要找最右下角的值,他就是答案~
贴个当年博主初学背包刷题的code
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int DP[1002][1001];
int bone[1001][2]={0};
int main(){
int str,n_bone,v_bag;
cin>>str;
while(str--){
memset(DP,0,sizeof(DP));
memset(bone,0,sizeof(bone));
cin>>n_bone>>v_bag;
for(int i=1;i<=n_bone;++i)
scanf("%d",&bone[i][0]);
for(int i=1;i<=n_bone;++i)
scanf("%d",&bone[i][1]);
for(int i=1;i<=n_bone;++i){
int t_v=bone[i][0];
int t_w=bone[i][1];
for(int j=0;j<=v_bag;++j){
if(j<t_w)DP[i][j]=DP[i-1][j]; //当前状态无法放下此骨,就沿用上一行的最大值
else DP[i][j]=max(DP[i-1][j],t_v+DP[i-1][j-t_w]); //能放下,就比较不取用这根骨头和取用这根骨头在当前状态下哪个能取得更大价值
}
}
cout<<DP[n_bone][v_bag]<<endl;
}
}
覆盖法
在打上面的表格时,我们发现只有上一行的数据才能对新一行的数据产生影响;状态转移公式中我们也可以看出数据影响的特点:
- 同列数据影响:上一行数据影响新一行
- 不同列数据影响:上一行左侧数据影响新一行数据
所以我们可以用一个一维数组来代替二维数组,新数据在同一数组中覆盖代替,但只要从右往左修改覆盖数据,就不会产生不良影响
why?
因为如果从左往右,你可能在前面已经使用了此物品,但依旧在后面使用该物品第二次,第三次…也就是你隐性地把一件物品重复使用,破坏了 01 背包一件物品只有一次使用机会的条件设定 ,达到了一物多用,废物二用,循环再生,螺旋升天,法力无边的境界
一件物品不限次数的使用那就是完全背包问题了呀!!
哎呀我丢!一下子把完全背包也讲了…
那么 01 背包就是从右到左刷新数据,
完全背包 就是从左到右刷新数据
嗯,就是酱紫(~ ̄▽ ̄)~
我们来看看时间复杂度和空间复杂度的表现:
时间复杂度不变 O ( W * V )
空间复杂度从二维数组的 O ( (W + 1) * ( V + 1 ) ) 变为 O ( V + 1)
这覆盖法的空间优化是不是整挺好<( ̄︶ ̄)>
多重背包
每一种物品能取的量题目给定,求最大价值
暴力 01 背包法
一样的物品和不一样的物品都用同一种处理法,也就是放弃多重背包的特点,退一步,从 01 背包的视角去处理问题 ( ̄. ̄)
更好的方法 ↓
二进制优化法
设一个数 n ,那么 0 ~ 2n 中的任意一个数都可以用 2x +2y +2z + …(x, y, z…<n) 的形式表达出来
你觉得很神奇,是吗 (●゚ω゚●)
但 这不就是 二进制本质吗
难道你怀疑有些数只能用十进制表示而不能用二进制表示吗  ̄ω ̄=
假设题目中给出一种物品,其价值 V,代价 W,数量 m = 65 ,我们也知道了这种物品在最优方案中应该取用 a 个。众所周知,01 背包的解决方案是 +1 +1 +1 … 直到 a 个(非常典雅但不高效)。
二进制优化:我们将 65 进行二进制拆分。
二进制 | 20 | 21 | 22 | 23 | 24 | 25 | 剩 |
---|---|---|---|---|---|---|---|
拆分单位 | 1 | 2 | 4 | 8 | 16 | 32 | 2 |
剩余量 | 64 | 62 | 58 | 50 | 34 | 2 | 0 |
我们只要用表中的 7 个拆分单位 1, 2, 4, 8, 16, 32, 2 就可以组合出任一 0 ~ 65 之间的数
不要问我为什么,这问题就和我为什么这么帅一样,无需回答╮( ̄▽ ̄)╭
那么相应的,我们把 m = 65 个物品,打包成 1,2,4,8,16,32,2的样子(你可以理解为用绳子把他们捆起来让他们永远的变成二进制的亚子)然后他们就变成了 7 个不同的物品:
物品 | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
价值 | 1*V | 2*V | 4*V | 8*V | 16*V | 32*V | 2*V |
代价 | 1*W | 2*W | 4*W | 8*W | 16*W | 32*W | 2*W |
而且我保证,只要你敢拿来用,它就会变成你想要它变成的亚子(╯▽╰)
这时可能就有小伙伴要问了,为什么是二进制而不是三进制、四进制捏?(・ε・`*)
划重点(附带敲黑板:
01 背包中,我们解释了其名称的由来。0 和 1 ,代表无和有,就是意味着取和不取。我们用二进制拆分,无非是简化了多重背包,让多重背包回到 01背包的解题模式,但又巧妙利用了其性质,避开了暴力的路径,将处理单种物品的复杂度从 暴力的 O( m )级别降到 O( log m )级别。超过二的进制就超过了 01 两种状态,不符合 01 背包的特征,所以我们采用二进制。
完全背包
不好意思,上面?讲 01的时候不小心讲出来了。内容是少了点,但是全部了。
从左到右覆盖数据(完全背包用 覆盖法 来做,不打二维表)
代码
三种背包全在咯
//这是博主当年初学背包时找到的一位dalao的代码,很简洁很清晰,我很中意的啦
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define N 1000 //物品个数
#define M 100000000 //所有物品可能的最大价值
int m[N],c[N],w[N],f[M],V;
void ZeroOnePack(int cost,int weight);
void CompletePack(int cost,int weight);
void MultiplePack(int cost,int weight,int amount);
int main()
{
int n,i;
scanf("%d %d",&n,&V);
// 两种不同的初始化方式,根据情况自行选择
//memset(f,0,sizeof(f[0])*(V+1)); // 只希望价格尽量大
//memset(f,-M,sizeof(f[0])*(V+1));f[0]=0; // 要求恰好装满背包
for(i=0;i<n;i++) scanf("%d %d %d",m+i,c+i,w+i);
for(i=0;i<n;i++) MultiplePack(c[i],w[i],m[i]);
printf("%d\n",f[V]);
system("PAUSE");
return 0;
}
void ZeroOnePack(int cost,int weight)
{
int v;
for(v=V;v>=cost;v--)
f[v]=max(f[v],f[v-cost]+weight);
}
void CompletePack(int cost,int weight)
{
int v;
for(v=cost;v<=V;v++)
f[v]=max(f[v],f[v-cost]+weight);
}
void MultiplePack(int cost,int weight,int amount)
{
int k;
if(cost*amount>=V)
{
CompletePack(cost,weight);
return;
}
k=1;
while(k<amount)
{
ZeroOnePack(k*cost,k*weight);
amount=amount-k;
k=k*2;
}
ZeroOnePack(amount*cost,amount*weight);
}
当然,背包问题远不止这些,楼教主的男人九题,我也至今无果(啊~那不是说明…
让我推荐的话我也找不出好的。因为神仙打架嘛!我也看不懂的就不瞎bibi了
如果以后有精力的话,或许会更新这篇博客的,让它变成九讲背包讲解 []( ̄▽ ̄)*
还有,欢迎和博主交流哦( ̄▽ ̄)"
版子题 也就是传说中的模版题,可以将单纯一种算法直接僵硬地套在其中,深受
菜鸟做题人的喜爱。大家看见版子题一般会大喊一声:哇!金色传说~
不过赛场上,你压根就看不出他是个版子题 (つД`)・゚・ ↩︎