一、实验目的
1.掌握基于回溯的算法求解0-1背包问题的原理。
2.掌握编写回溯法求解0-1背包问题函数的具体步骤并理解回溯法的核心思想以及其求解过程。
3.掌握子集树以及其他几种解空间树的回溯方法并具备运用回溯算法的思想设计算法并用于求解其他实际应用问题的能力。
4.深刻体会回溯算法求解问题的便利以及感受使用回溯算法所编写程序的明确结构和良好的可读性。
5.从算法设计分析角度,体验用不同解法(动态规划,回溯法)求解问题的方法和思路,从而对0-1背包问题基于回溯法求解有更进一步的理解。
二、实验环境
操作系统:Windows10
文本编辑器:VisualStudio Code
所用语言和编译器:C++ g++
实验终端:WindowsPowerShell
三、实验内容
现在给定N个物品,每个物品的重量为w(即物品i的重量为wi),给定一个背包,背包的容量为c,0-1背包问题要解决的是,通过选择装入背包中的物品,使得所选择物品的重量之和w在小于等于背包容量的情况下,使得装入背包的物品的价值为所有选择之中最大的那个。
0-1背包问题有如下限制,对于每种物品i,有两种选择,装入或不装如(装入为1,不进行装入为0),既不能装入多次,也不能只装入一部分。
I |
1 |
2 |
3 |
4 |
W(重量) |
3 |
5 |
2 |
1 |
P(价值) |
9 |
10 |
7 |
4 |
例如若当前背包容量为9,有4个物体,各个物体的重量和价值如上图所示:则通过分析可知,最优解为装入价值为9,10,4的物品,最优价值为23,下面将通过回溯算法实现求解该01背包问题。
程序输入,首先输入物品的数量n和背包的容量c,并依次输入n个物品的重量wi和每个物品的价值pi,程序要求输出为可以得到的最优价值和达到最优值所装入的物品0-1序列。
四、算法描述
通过分析问题可知,0-1背包问题为整数线性规划中的01规划问题,通过建模,可以得出01背包问题的形式化描述如下 :给定c > 0, wi > 0, vi > 0, 1<= i <=n,所求为
对于回溯算法,将这n个物品装入背包,每个物品对应两个状态,装入背包或不装入背包。第i中物品对应(x1,x2,…,xn),其中xi可以取0或1,分别表示将该物品装入背包或不装入背包。解空间有2^n种可能的解法,也就是n个元素组成的集合所有子集的个数,该集合可以采用一棵满二叉树将该解空间组织起来,解空间的深度为问题的规模n。
使用回溯法求解0-1背包问题需要通过回溯枚举所有的解空间,如果不通过剪枝以减少计算量的话,枚举所有解空间所需要的时间复杂度为O(2^n),总共有n个物品,按顺序依次考虑每个物品,这样就形成了一棵解空间树。如图所示:
然而进一步分析可知,很多情况下的回溯是没有意义的,比如当物品重量大于背包容量时,没有必要再进一步对剩余的物品进行回溯决策了,同时若剩下的所有物品都取其总价值也没有当前已经求得的物品选择方案价值大,也可以停止回溯,即该回溯算法求解0-1背包问题存在优化空间,可以利用剪枝来减少计算量并降低时间复杂度。
构造出问题的状态空间树以后,就可以从其根节点出发来搜索解空间,为使目标函数值增加的最快,可以每次优先选择价值最大的物品装入背包,直到背包装不下,但是若选择的物品重量很大,则背包剩余空间消耗的太快,又使得继续装入背包的物品在满足约束方程的要求后,却无法达到目标函数的要求。
为解决这个问题,首先应该把所有的物品按价值重量比非递增排序,然后按照这个顺序进行搜索,在装入背包的过程中,尽量优先选择价值重量比较高的物品装入背包。即在搜索过程中,尽量沿着左子树结点前进,当不能继续前进时,就会得到问题的一个部分解(设该节点为T)。然后搜索右子树,估计由该部分分解所能达到的最大价值,即结点T的上限。
对于剩余物品的处理,将按照价值重量比以非递增的顺序依次装入背包,到无法完全装入下一个物品时,就将该物品的一部分装满背包,这样就可以得到一个上限,如果改制为当前最优值,继续从右子树向下搜索,以扩大部分解直到找到可行解,保存可行解,并把可行解的值作为当前最优值,向上回溯,寻找其他可行解,若该值小于当前最优值,则丢弃当前正在搜索的部分解,向上回溯。反复使用当前方法,直到搜索完所有解空间。
求解0-1背包问题的回溯函数可以提取为如下几个步骤:
回溯算法(backtrack)
如果到达边界:
记录当前的结果,进行处理
如果没有到达边界:
如果满足限界条件:(左子树)
进行处理
进行下一层的递归求解
将处理回退到处理之前
如果不满足限界条件:(右子树)
进行下一层递归处理
Backtrack函数在搜索状态空间树时,只要左子节点是可一个可行结点,搜索就进入其左子树。对于右子树时,先计算上界函数,以判断是否将其减去(即剪枝)其具体步骤可以描述如下:
:回溯函数backtrack形参使用i来记录当前回溯所到达的层数,同时也记录的当前记录了几个物品。
:判断是否到达边界,如果到达边界,对bestp进行赋值,并结束递归,回溯函数返回。
:判断左子结点是否可行,若可行则搜索左子树,搜索时将物品i放入背包,并更新当前背包重量和总价值,然后进入下一层的搜索,搜索完毕后回溯复原。
:对于右子树,使用上界函数判断右子树是否符合条件(bound(i+1)>bestp),记录当前put[i] = 0,然后进行右子树的搜索。
代码逻辑如下:
void backtrack(int i)
{ //i用来指示到达的层数和当前选择了哪几个物品
// doublebound(int i);
if(i>n) //递归结束的判定条件
{
bestp = cp;
return;
} //如若左子节点可行,则直接搜索左子树;
//对于右子树,先计算上界函数,以判断是否将其减去
if(cw+w[i]<=c)//将物品i放入背包,搜索左子树
{
put[i] = 1;
cw+=w[i];//更新当前背包的重量
cp+=p[i];//更新当前背包的总价值
backtrack(i+1);//搜索下一层
cw-=w[i];
cp-=p[i];//回溯复原
}
if(bound(i+1)>bestp)//如若符合条件则搜索右子树
{
put[i] = 0;
backtrack(i+1);
}
对于bound函数,该函数用来判断当前背包的总价值cp和背包剩余所可以容纳的最大价值是否小于等于当前的最优价值bestp,同时装入物品时,是以剩余物品单位重量价值递减的次序进行装入。代码逻辑如下:
:使用cleft变量表示当前背包剩余容量(cleft=c-cw)cw为当前背包容量,并使用变量b记录当前背包的总价值cp,用以计算上界。
:以物品单位重量价值递减次序装入物品。
:判断是否装满背包并返回计算出的上界b。
代码逻辑如下:
double bound(int i)
{ //判断当前背包的总价值cp+剩余容量可容纳的最大价值<=当前最优价值
double cleft=c-cw;//剩余背包容量
double b =cp;//记录当前背包的总价值cp,最后求上界
//以物品单位重量价值递减次序装入物品
while(i<=n&& w[i]<=cleft)
{
cleft-=w[i];
b+=p[i];
i++;
}
//装满背包
if(i<=n)
b+=p[i]/w[i]*cleft;
return b;//返回计算出的上界
}
回溯开始前,使用函数sortArr()对价值数组,重量数组,和单位重量价值比函数进行排序。对着三者的排序,可以在一次冒泡排序中同时解决。算法步骤如下:
:计算每个物品的单位价值(单位重量的物品价值)。
② :冒泡按照价值重量比非递增的顺序对价值数组,重量数组和单位价值数组进行排序。
代码逻辑如下:
voidsortArr()
{
for(int i=1;i<=n;i++)
perp[i]=p[i]/w[i]; //计算单位价值(单位重量的物品价值)
for(int i=1;i<=n-1;i++)
{
for(int j=i+1;j<=n;j++)
if(perp[i]<perp[j])
{
swap(perp[i], perp[j]); //交换单位价值数组元素
swap(p[i],p[j]); //交换价值数组元素
swap(w[i],w[j]); //交换重量数组元素
}
}
}
算法的时间复杂度,计算上界函数bound需要O(n)的时间,最坏情况下有O(2^n)个右儿子节点需要计算上界函数,故解决0-1背包问题的回溯算法的时间复杂度为O(n2^n)。
五、实验结果
第一组输入,设输入物品的数量和背包的容量为n = 4,c = 9,各个物品的重量w为3,5,2,1,各个物品的价值p为9,10,7,4。则通过回溯算法搜索迭代求解可知,背包问题的最优价值为23,需要装入的物品编号为1,0,1,1(对应排好序后的物品价值序列),装入的物品为价值为9,4,10。
第二组输入,设输入物品的数量和背包的容量为n = 4,c = 1000(背包容量远大于各个物品的重量),各个物品的重量w为1,2,3,4,各个物品的价值p为99,99,99,99。则通过回溯算法搜索迭代求解可知,背包问题的最优价值为396,需要装入的物品编号为1,1,1,1(对应排好序后的物品价值序列),即将所有的物品都装入。
第三组输入,设输入物品的数量和背包的容量为n = 4,c =0(背包容量远小于各个物品的重量),各个物品的重量w为1,2,3,4,各个物品的价值p为100,100,100,100。则通过回溯算法搜索迭代求解可知,背包问题的最优价值为0,所有的物品都无法装入。
六、实验总结
本次实验从0-1背包问题基于回溯算法求解出发,生动形象的展示了回溯算法在生活中的实用性。关于最0-1背包问题,有多种算法可以求解,比如前面学过的动态规划算法,在动态规划算法中,其时间复杂度为O(nc),在回溯算法求解的实现中,其时间复杂度为O(n2^n),通过两次使用不同的算法求解相同的问题,增加了我对于这两种算法和对0-1背包问题的理解。
在本次的代码实现中,遇到了两个错误,第一个错误是实现sortArr函数时,要对三个数组进行非递增的冒泡排序,在其中价值数组的交换时,误将j写成i,导致结果出错,最后通过debug发现,在排序时,根本没有进行交换,在修改完毕后,为了防止此问题再次出现,我将交换逻辑修改为swap函数实现,以更快的找到错误。
第二个问题是选择是否装入物品的数组的输出问题,由于输出顺序出错,导致选出的最优价值是正确的,而选则的最优数组确实错误的(0和1的个数是正确的,顺序却是错误的),最后经过排查后该问题也得以解决。
通过这次实验,我也深刻感受到使用回溯算法求解相关问题的好处,使用回溯算法求解,代码结构清晰,思路明确且易于理解,并且通过限界函数和约束条件剪枝减少了很多不必要的计算。使用回溯算法求解问题便利高效。
回溯算法的递归通过系统递归栈实现,增加了空间复杂度,但使用递归可以明显减少代码的编写复杂程度,同时使用递归编程时,应注意递归结束条件的编写,防止程序无限运行,造成计算资源的浪费。
通过本次实验,增加了我对限界函数和约束条件剪枝对于减少计算量的认知(求解时间大幅减少),以及在编程中,应根据实际问题出发,通过对比各种实现方式,选取高效简洁同时安全的实现。这些优化空间启发着我不断学习,尝试各种实现和优化,并且对于求解算法问题的道路上遇到的各种问题,应不断求索以实现自我进步。文章来源:https://www.toymoban.com/news/detail-476728.html
代码如下:文章来源地址https://www.toymoban.com/news/detail-476728.html
#include <iostream>
#include <stdio.h>
#include <algorithm>
using namespace std;
const int maxn = 100;
int n;//物品数量
double c;//背包容量
double p[maxn], w[maxn];//物品的价值和重量
double cw = 0.0, cp = 0.0;//当前背包重量和当前背包中物品总价值
double bestp = 0.0;//当前最优价值best price
double perp[maxn];//单位物品价值(排序后) per price
int put[maxn];//设置是否装入,为1的时候表示选择该组数据装入,为0的表示不选择该组数据
double bound(int i)
{ //判断当前背包的总价值cp+剩余容量可容纳的最大价值<=当前最优价值
double cleft= c-cw;//剩余背包容量
double b = cp;//记录当前背包的总价值cp,最后求上界
//以物品单位重量价值递减次序装入物品
while(i<=n && w[i]<=cleft)
{
cleft-=w[i];
b+=p[i];
i++;
}
//装满背包
if(i<=n)
b+=p[i]/w[i]*cleft;
return b;//返回计算出的上界
}
void sortArr()
{
for(int i=1;i<=n;i++)
perp[i]=p[i]/w[i];
for(int i=1;i<=n-1;i++)
{
for(int j=i+1;j<=n;j++)
if(perp[i]<perp[j])
{
swap(perp[i], perp[j]);
swap(p[i],p[j]);
swap(w[i],w[j]);
}
}
}
//回溯函数
void backtrack(int i)
{
if(i>n) //递归结束的判定条件
{
bestp = cp;
return;
}
//如若左子节点可行,则直接搜索左子树;
//对于右子树,先计算上界函数,以判断是否将其减去
if(cw+w[i]<=c)//将物品i放入背包,搜索左子树
{
put[i] = 1;
cw+=w[i];//同步更新当前背包的重量
cp+=p[i];//同步更新当前背包的总价值
backtrack(i+1);//深度搜索进入下一层
cw-=w[i];//回溯复原
cp-=p[i];//回溯复原
}
if(bound(i+1)>bestp)//如若符合条件则搜索右子树
{
put[i] = 0;
backtrack(i+1);
}
}
int main()
{
cout << "输入物品的数量和背包的容量:" << endl;
cin >> n >> c;
cout << "请依次输入物品的重量:" << endl;
for (int i = 1; i <= n; i++)
cin >> w[i];
cout << "请依次输入物品的价值:" << endl;
for (int i = 1; i <= n; i++)
cin >> p[i];
sortArr();
backtrack(1);
printf("最优价值为:%1f\n",bestp);
printf("需要装入的物品编号是:");
printf("%d ", put[4]);
for (int i = 1; i <n; i++)
printf("%d ",put[i]);
return 0;
}
到了这里,关于基于回溯法求解0-1背包问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!