题目描述
有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
举一个例子:
有一个小偷他有一个容量为8的背包
物体的体积和价值如下所示:
物体编号 | 物体名称 | 体积 | 价值 |
---|---|---|---|
1 | 手机 | 2 | 3 |
2 | 平板 | 3 | 4 |
3 | 相机 | 4 | 5 |
4 | 电脑 | 5 | 8 |
问小偷如何偷,偷的总价值最高?
思路
你站在小偷的角度思考一下,情况有这几种:
- 第一种这个物品装不下。没法拿。
- 第二种这个物品可以装下。但是到底拿不拿?
解决办法
用动态规划的方法来解决这个问题。
动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
最优性原理是动态规划的基础,最优性原理是指"多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略。"
动态规划简言之就是: 将小问题依次递推到我们的最终问题。由于小问题都是最优解。
那么最终的问题一定也是最优解。
实战分析
题目的大致分析和动态规划的原理都知道了。接下来,我们就用上面的例子来实际的分析一波。
记f(k,w): 当背包容量为w,现有k件物品可以偷。
例: f(4,8) 的意思是有4件可以偷,背包的容量为8。
从物体编号4,3,2,1依次遍历
首先定义一些变量:Vk表示第 k个物品的价值,Wk表示第 k个物品的体积,定义f(k,w):当前背包容量 w,现有k件物品可以偷,前 k个物品可选的最佳组合对应的价值。
面对当前商品有两种可能性:
- 包的容量比该商品体积小,装不下,此时的价值与前k-1个的价值是一样的,即f(k,w)=f(k-1,w)
简言之就是说: 小偷偷这个东西装不下,即不能选这个物品了,那么此时价值是不会多的,且偷的选项少了一个。 - 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即f(k,w)=max{f(k-1,w),f(k-1,w-wk)+vk}。
由上图可以得到如下状态转移方程:
这里需要解释一下,为什么能装的情况下,需要这样求解(这才是本问题的关键所在!):
可以这么理解,如果要到达f(k,w)这一个状态有几种方式?
肯定是两种,第一种是第k件商品没有装进去,第二种是第k件商品装进去了。没有装进去很好理解,就是f(k-1,w);装进去了怎么理解呢?如果装进去第k件商品,那么装入之前是什么状态,肯定是f(k-1,w-w(k))。由于最优性原理(上文讲到),f(k-1,w-w(k))就是前面决策造成的一种状态,后面的决策就要构成最优策略。两种情况进行比较,得出最优。
填表,首先初始化边界条件,f(0,w)=f(k,0)=0;
然后一行一行的填表:
- 如,k=1,w=1,w(1)=2,v(1)=3,有w<w(1),故f(1,1)=f(1-1,1)=0;
- 又如k=1,w=2,w(1)=2,v(1)=3,有w=w(1),故f(1,2)=max{
f(1-1,2),f(1-1,2-w(1))+v(1) }=max{0,f(0,0)+3}=max{0,0+3}=3; - 如此下去,填到最后一个,k=4,w=8,w(4)=5,v(4)=8,有w>w(4),故f(4,8)=max{
f(4-1,8),f(4-1,8-w(4))+v(4) }=max{f(3,8),f(3,3)+8}=max{9,4+8}=12
根据状态转移方程可以得出如下表
你如果仔细的看一下你会发现,表中的每一个空都是当前的最优解。
而后面的最优解都是由前面的最优解依次递推出来的。
由此每一个空都是当前下的最优解。
例: f(3,5)代表的就是,当背包容量为5,前三个物品可以选时的最优解。
f(4,8)就是当背包容量为8,有四个物品可以选时的最优解。
代码实现
#include<stdio.h>
#define N 5
#define W 9
int B[N][W]={0};
int w[5]={0,2,3,4,5};
int v[5]={0,3,4,5,8};
void max()
{
int k,c;
int value1,value2;
for(k=1;k<N;k++)//1-4
{
for(c=1;c<W;c++)//1-8
{
if(w[k]>c)//如果说当前商品的重量大于背包剩余的数量,即:无法偷
{
B[k][c]=B[k-1][c];//价值等于上次的价值
}
else//可以偷
{
value1=B[k-1][c-w[k]]+v[k];//偷的话的价值
value2=B[k-1][c];//不偷的话的价值
if(value1>value2)//看哪个最优
{
B[k][c]=value1;
}
else
{
B[k][c]=value2;
}
}
}
}
}
int main()
{
max();
//printf("%d\n",B[4][8]);
int i,j;
for(i=0;i<N;i++)
{
for(j=0;j<W;j++)
{
printf("%d ",B[i][j]);
}
printf("\n");
}
}
背包问题最优解回溯
通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:
f(k,w)=f(k-1,w)时,说明没有选择第k个商品,则回到f(k-1,w);
f(k,w)=f(k-1,w-w(k))+v(k)时,说明装了第k个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,
即回到f(k-1,w-w(k));
一直遍历到k=0结束为止,所有解的组成都会找到。
就拿上面的例子来说吧:
最优解为f(4,8)=12,而f(4,8)!=f(3,8)却有f(4,8)=f(3,8-w(4))+v(4)=f(3,3)+8=4+8=12,所以第4件商品被选中,并且回到f(3,8-w(4))=f(3,3);
有f(3,3)=f(2,3)=4,所以第3件商品没被选择,回到f(2,3);
而f(2,3)!=f(1,3)却有f(2,3)=f(1,3-w(2))+v(2)=f(1,0)+4=0+4=4,所以第2件商品被选中,并且回到f(1,3-w(2))=f(1,0);
有f(1,0)=f(0,0)=0,所以第1件商品没被选择。
最终代码实现
#include<stdio.h>
#define N 5
//N代表共有几件可选的数量加1,因为数组的下标是从0开始的
//为了方便所以为 4+1 为5
#define W 9
//W为当前背包的容量,同样的为了方便加1
int B[N][W]={0};//需要填的表
int intem[N]={0};//存放最优解的情况
int w[5]={0,2,3,4,5};//物体体积的数组
int v[5]={0,3,4,5,8};//物体价值的数组
void max();
void print();
void find(int k,int c);
void max()
{
int k,c;//k代表有几个可以选,c代表当前的背包体积
int value1,value2;
for(k=1;k<N;k++)//1-4
{
for(c=1;c<W;c++)//1-8
{
if(w[k]>c)//如果说当前商品的重量大于背包剩余的数量,即:无法偷
{
B[k][c]=B[k-1][c];//价值等于上次的价值
}
else//可以偷
{
value1=B[k-1][c-w[k]]+v[k];//偷的话的价值
value2=B[k-1][c];//不偷的话的价值
if(value1>value2)//看哪个最优
{
B[k][c]=value1;
}
else
{
B[k][c]=value2;
}
}
}
}
}
void find(int k,int c)//找最优解
{
if(k>0)
{
if(B[k][c]==B[k-1][c])//说明该物品没选
{
intem[k]=0;
find(k-1,c);//往下接着递归查找
}
if( (c-w[k]>=0) && (B[k][c]==B[k][c-w[k]]+v[k]) )//说明该物体选了
//
{
intem[k]=1;
find(k-1,c-w[k]);
}
}
}
void print()
{
int i;
printf("最大价值为:%d\n",B[N-1][W-1]);
printf("选的物体的编号为:");
for(i=1;i<N;i++)
{
if(intem[i]==1)
{
printf("%d ",i);
}
}
printf("\n");
}
int main()
{
max();
find(4,8);
print();
}
效果图如下:
实战
光看不练假把式,我们看一道题实战一下。
看了题目你会发现这就是一个0/1背包问题。不过和我们上面讲的好像有点不一样。
但其实大同小异。
总结我们上面所学的动态规划(DP) 无非这几步:
- 分析状态方程
- 画表的边界
- 画表
分析状态方程
我们上面的f(k,w)代表的是最大的价值。
而本问题问的是总的方法数,即最大的方法数。
故设 f(k,w): k代表有几个可选 w代表背包容量
f(k,w)代表当有k个物品可以选时,凑成体积为w的方法数
设Vk表示第 k个物品的体积
通过分析你会发现面对当前商品只有两种可能性:
拿与不拿
- 不拿:包的容量比该商品体积小,装不下,此时的方法数与前k-1个的方法数是一样的,即f(k,w)=f(k-1,w)
简言之就是说: 这个东西装不下,即不能选这个物品了,那么此时方法数是不会多的,且能选的物体数少了一个。 - 拿:还有足够的容量可以装该商品.所以它的方法数等于拿的方法数+不拿的方法总数
即f(k,w)=f(k-1,w)+f(k-1,w-v[k])
表的边界
当包的体积为0时啥也装不下。只有一种情况
故 f(k,0)=1;
当物体可选的为0时,一种情况也没有
故 f(0,w)=0;
最后画表的过程我这里就省略了。
下面直接上代码
#include<stdio.h>
#define N 21
#define W 41
int f[N][W]={0};
int a[N]={0};
int main(void)
{
int m;
int k,w;
while( scanf("%d",&m) != EOF )
{
for(k=1;k<=m;k++)
{
scanf("%d",&a[k]);
}
for(w=0;w<W;w++)//初始化边界
{
f[0][w]=0;
}
for(k=0;k<N;k++)//初始化边界
{
f[k][0]=1;
}
for(k=1;k<=m;k++)
{
for(w=1;w<W;w++)
{
if(a[k]>w)//不能拿
{
f[k][w]=f[k-1][w];
}
else//可以拿
{
f[k][w]=f[k-1][w]+f[k-1][w-a[k]];//不拿的方法数+拿的方法数
}
}
}
printf("%d\n",f[m][40]);
}
return 0;
}
最后
本篇文章借鉴于这个大佬写的文章https://blog.csdn.net/qq_38410730/article/details/81667885
这个大佬写的很好。
我是按照我的理解在这位大佬的基础上做了补充。