算法:动态规划窃贼问题C语言实现
目录
第一章 问题描述
1.1问题描述
有一个窃贼带着一个背包去偷东西,房屋中共有5件商品,其重量和价值如下:
物品1:6公斤,48元。
物品2:5公斤,40元。
物品3:2公斤,12元。
物品4:1公斤,8元。
物品5:1公斤,7元。
窃贼希望能够拿最大价值的东西,而窃贼的背包最多可装重量8公斤的物品,那么窃贼应该怎么装上面商品才能达到要求呢?
第二章 算法思想及算法设计分析
2.1算法思想
根据动态规划解题步骤(问题结构分析、递推关系建立、自底向上计算、最优方案追踪)找出01背包问题的最优解以及解组成。
-
首先,明确问题,前i个商品可选、求背包容量为c时的最大总价格。
-
其次,分析最优结构,构造递推公式,问题的最优解由相关子问题最优解组合而成,子问题可以独立求解。构造递推公式:
P[i,c]=max{P[i-1,c],P[i-1,c-v[i]]+p[i]}
-
然后,确定计算顺序,依次求解问题,由于本问题可以看成是五个子问题,又因本问题的解中包含前一项问题的最优解,所以我们可以从最简单的子问题开始计算,依次增加问题难度。
-
最后,记录决策过程,输出最优方案。
2.2设计算法
我们可以将问题拆分为五个子问题。
-
背容=8,从1号物品中找出该问题的解
-
背容=8,从1号,2号物品中找出该问题的解
-
背容=8,从1号,2号,3号物品中找出该问题的解
-
背容=8,从1号,2号,3号,4号物品中找出该问题的解
-
背容=8,从1号,2号,3号,4号,5号物品中找出该问题的解1,2,
3,4,5子问题的答案都存入一张表中。
- 因为求解2子问题,需要用到1子问题的答案(2的每一步方案要与1的每一步方案比较,如何2的该步方案优于1所对应的方案。则将2的这步方案标为可行。如果不优于1的,或者不满足问题的约束条件,则舍弃该方案。继续沿用该步所对应的1的方案作为该步的方案)。求解3子问题,需要用到2子问题的答案,一直递推到求解5子问题,需要用到4子问题的答案。而5子问题就是原问题。5子问题的答案就是最终原问题的解。
2.3算法分析
- 通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
2.4填表结果
- 为了便于后面讲解填表方法,故将物品顺序倒置,即物品5为物品1.物品4为物品2,物品3为物品3,物品2为物品4,物品1为物品5。所以有下方两表。
表2-1 动态规划表
P[i,c] | c=0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
i=0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 7 |
2 | 0 | 8 | 15 | 15 | 15 | 15 | 15 | 15 | 15 |
3 | 0 | 8 | 15 | 20 | 27 | 27 | 27 | 27 | 27 |
4 | 0 | 8 | 15 | 20 | 27 | 40 | 48 | 55 | 60 |
5 | 0 | 8 | 15 | 20 | 27 | 40 | 48 | 56 | 63 |
表2-2 决策表
Rec[i,c] | c=0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
i=1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
3 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
4 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
5 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
2.5填表方法
2.5.1创建重量价值表
- 先将价值和重量编程一个表,便于填表使用。
表2-3 重量价值表
w[i] | 1 | 1 | 2 | 5 | 6 |
---|---|---|---|---|---|
p[i] | 7 | 8 | 12 | 40 | 48 |
2.5.2初始化
- 程序初始化的过程就是将第0行的所有数填为0,这是符合现实情况的。它表明当前背包没有容量时价值为0,当前背包没有商品时价值也是0。
2.5.3使用递推公式填表
-
拿P[3][3]=20来说明填表的过程。P[3][c]表明i=3(当前子问题有3个物品可选,分别是1,2,3号物品)。我要选的3号物品的重量是2,它的价值是12,所以我会找到它的前2列的上一行所对应的背包的价值(P[3][1])+12(当前要选的3号物品的价值)=20,值为20>P[2][8]=15,代表我选了2,3的方案要优于选1,2的这种方案,所以我将20填入到表格。如果P[3][1]+12<15,则将15填入表格。
-
这里需要说明一下P[2][8]=15的含义:2子问题的一个解是{1,2}选择1,2号物品所对应的背包价值为15。恰巧我们在解决3子问题时{1,2,3}需要计算比较这几种方案,选物品{1,3}的背包中的重量是多少?背包中的价值是多少?选物品{2,3}的背包中的重量是多少?背包中的价值是多少?选物品{1,2,3}的背包中的重量是多少?背包中的价值是多少?而物品{1,2}的背包价值在2子问题中已给出,因此我们可以在3子问题中直接用。但是为何不考虑选择{1,3}物品呢?就是拿1号和3号物品组队放入背包,因为在2子问题中已经记载了。
-
如果要单选一个物品放入背包的话,选择2号获得的价值要比1号获得的价值大----我们追溯到P[2][2]=8是这么来的,所以P[2][2]是8,而不是7,所以它相当于定义说:下一个子问题在求解的过程中,如果遇到只能从1号和2号物品中选择一个物品装入背包时,请选择2号物品。
-
因为是问题3有三种选择,当前两种物品1,2选择后背包剩余的可负载重量还有8-2=6>4所以在背包容量c=4的时候,可以装下三个物品,即
P[3,4]=max{P[i-1,c],P[i-1,c-v[i]]+p[i]}
=max{P[2,4],P[2,2]+12}
=max{15,15+12}
=max{15,27},
取价值高的填入表中,即将值27填入表中。
2.5.4决策表填写方法
- 在记录最优值的时候,同时完成决策表,决策表中,1表示选择第i个商品,0表示不选择第i个商品。公式为:
Rec[i,c]={1,选择物品i
0,不选择物品i }
我们同样以P[3,8]的填表过程举例:
- 当我们填决策表Rec[3,0],Rec[3,1],Rec[3,2]时因为空间不够,无法选择第3个物品,所以均填0,填写Rec[3,3]因为,在填写P[3,3]时选择的是第2个和第3个为物品,选择了第i个物品(第3个),所以Rec[3,3]处填入1,在填写P[3,4]时选择的是第1个、第2个和第3个为物品,所以Rec[3,4]处填入1 ,在往后面填写过程中,只增加了背包的重量上界,但是可选的物品还是三种,且将三种物品完全选择,即都包含第3种物品,所以剩下的位置也都填1。
第三章 程序编码与复杂性分析
3.1程序编码
#include <stdio.h>
#define N 6 //定义动态规划表的行,方便修改
#define C 9 //定义动态规划表的列,方便修改
void Knapsack(int n,int c,int *w,int *p) //0-1背包算法子函数,参数为背包容量c,物品个数n,物重w的数组,物价p的数组
{
int P[N][C] = {{0}}; //创建动态规划表,初始值赋为零,是个二维表
int Rec[N-1][C] = {{0}};//创建决策表,初始值为0,是个二维表,比动态规划表少一行
int i=0,j=0;//生命变量i,j用于循环控制和最优值输出
//通过循环和递推公式将值填入到动态规划表中
for(i=1;i<=n;i++)//行的循环,循环五次
{
for(j=1;j<=c;j++)//列的循环,循环八次
{
P[i][j]=P[i-1][j];//将子问题的解赋给当前问题
if(j>=w[i])//判断背包此时的可负载重量,能否装下当前物品重量
{
if((P[i-1][j]) > (P[i-1][j-w[i]]+p[i]))//根据比较装下新物品后的值与子问题此时的值的大小选择递推公式
{
Rec[i-1][j] = 0;//此时相当于没有选择新物品所以为0
}
else
{
P[i][j] = P[i-1][j-w[i]]+p[i];//将通过递推公式得出的值赋给此时的问题,此时有更好的选择
Rec[i-1][j] = 1;//此时相当于选择了新物品所以为1
}
}
}
}
//因为i++和j++,循环结束,多加了一次i和j,所以最后要减回来
printf("背包能装的最大价值是:%d\n", P[i-1][j-1]);
printf("\n");//换行
//动态规划表输出
printf("动态规划表:\n");
for (int i = 0; i < N; i++)//因为生成的是N行C列的表所以循环N次和C次,进行输出
{
for (int j = 0; j < C; j++)
{
printf("%3d",P[i][j]);//从P[0][0]开始输出
}
printf("\n");//换行
}
printf("\n");//换行
//决策表输出
printf("决策表:\n");
for (int i = 0; i < N-1; i++)//因为决策表少一行所以外层循环少循环一次即N-1次
{
for (int j = 0; j < C; j++)
{
printf("%3d",Rec[i][j]);//从Rec[0][0]输出
}
printf("\n");//换行
}
printf("\n");//换行
//根据决策表回溯最优解的物品选择结果
int cout = 0;//声明计数变量
int Solves[N-1] ;//声明最优解所包含的物品的数组,因为物品最多有五个所以最多有N-1个解
//要从最后的结果回溯判断,即二维数组Rec[4][8]
int Reci = N-2;//声明决策表的循环控制变量,因为是只有5行所以是N-1,又因二维数组的表示要比二维表此时表示位置的大小减1所以是N-2
int Recj = C-1;//声明决策表循环控制变量,因为二维数组的表示要比二维表此时表示位置的大小减1所以是C-1
while(Reci != -1)//循环判断决策表此时的行数,因为决策表的最小行数是第0行,所以循环在-1时停下
{
if(Rec[Reci][Recj] == 1)//判断每个最优解中的最后一个物品是否被选择
//被选择
{
Solves[cout] = Reci + 1;//把此时的物品加入最优解所包含的物品的数组,因为Reci为二维数组的行数不是二维表的行数所以要加1
Recj = Recj-w[Reci+1];//因为要进行回溯,所以要把最终的重量减去当前所选的物品的重量得到背包剩下的重量,即决策表的列坐标
Reci = Reci-1;//因为要回溯到前一个问题,所以要减1到前一个问题
cout++;//计数变量自加
}
else
//不被选择
{
Reci = Reci-1;//因为当前物品没有被选择,背包重量不变所以列坐标不变,行坐标减1,直接考察下一个物品
//Recj = Recj;
}
}
//最优解个数输出
printf("最优解的构成:");
for(int i = 0; i < cout; i++)//循环次数为记录的数量
{
printf("物品%d ",Solves[i]);
}
}
int main()
{
int c; //声明变量
int n; //声明变量
c=8; //背包容量c
n=5; //物品个数n
//int w[6]={0,6,5,2,1,1}; //物重w,这样定义数组是为了方便使用物品的重量和价格,下标是几就是第几个物品
//int p[6]={0,48,40,12,8,7}; //物价p
int w[6]={0,1,1,2,5,6}; //物重w
int p[6]={0,7,8,12,40,48}; //物价p
Knapsack(n,c,w,p); //调用子函数
}
3.2复杂性分析
- 由于核心算法是由两层循环实现的,我们先分析内层循环。因为是动态规划表中的有效背包重量(不算背包能负载重量为零的时候)所以循环次数为c次,所以内层循环的时间复杂度是O©,现在我们分析外层循环。因为是动态规划表中的有效物品选择(不考虑一个物品都不选的时候),所以外层循环的时间复杂度为O(n),所以算法最终的时间复杂度为O(nc)。/
第四章 测试结果
4.1测试用例
- 为了便于分析问题,我们将物品顺序进行了倒转,所以最终我们相当于获得了两组测试用例,一组为原问题的测试用例,一组为问题倒转后的测试用例。
4.1.1原问题测试用例
表4-1原问题物品重量价值表
w | 6 | 5 | 2 | 1 | 1 |
---|---|---|---|---|---|
p | 48 | 40 | 12 | 8 | 7 |
4.1.2倒转物品顺序后的测试用例
表4-2倒转顺序后物品重量价值表
w | 1 | 1 | 2 | 5 | 6 |
---|---|---|---|---|---|
p | 7 | 8 | 12 | 40 | 48 |
4.2测试结果
4.2.1原问题测试结果
图4-1 原问题测试图
4.2.2倒转物品顺序后的测试结果
图4-2倒转物品顺序后的侧视图