- 问题描述
有n个重量分别为w1,w2,…,wn的物品(物品编号为1~n),它们的价值分别为v1,v2,…,vn,给定一个容量为W的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么选不中,要求选中的物品不仅能够放到背包中,而且具有最大价值,并对下表所示3个物品求出W=20时的所有解和最佳解。
物品编号 | 重量 | 价值 |
1 | 10 | 20 |
2 | 5 | 15 |
3 | 15 | 25 |
- 基本思路
- 回溯法基本思路
确定了解空间的组织结构后,回溯法从开始节点(根节点)出发,以深度优先搜索整个解空间。这个开始的节点为活节点,同时成为当前的扩展节点。在当前的扩展节点处,搜素向纵深方向移至一个新节点。这个新节点就成为新的活节点,并成为当前扩展节点。如果当前节点处不能再向纵深方向移动,则当前扩展节点为死节点。此时,应往回移动到最近的一个活节点处。回溯法以这种方式递归的在解空间中搜素,直至找到所有符合要求的解或解空间中已无活节点。(即深度优先搜素)
【优化方法】
剪枝(一):当前决策放入的物品总重量已经大于背包容量时,没有必要继续决策,此时可以将其左子树剪掉。
剪枝(二):如果将当前扩展节点后剩下的所有物品都装入还没有目前已求得的最优值大的话,就不在进行决策了,直接返回。
递归回溯时,在当前扩展节点处会通过设置约束函数和限界函数。不满足条件的,剪去相应的子树
- 实例
例:
假设:
物品个数为 n=3
背包的容量为 C=20
物品的重量分别为 w={10,5,15}
物品的价值分别为 v={20,15,25}
此时的解空间可以为(x1, x2, x3)的所有可能取{(0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)}
遍历解空间(深度优先)
从根往下遍历时,要求每次经过节点都计算此时是否满足容量的条件,当某个分支可以满足重量要求时,记录它的价值总量,以便最后选择最好的价值量。
例如第一次遍历时,经过x1=1,x2=1时,此时重量为15,第三个就放不进去了,那么我们使用剪枝算法的约束函数剪去x3=1这个分支,并遍历x3=0这个分支,再从x2=0往下拓展x3=1或者0的情况。其余节点都是按照此类方法依次辨别是否能放进背包中。
代码:
#include<iostream>
using namespace std;
#define M 105
int n;//表示物品个数
int W;//表示背包总载重量
double w[M];//表示对应物品重量
double v[M];//表示对应物品价值
double x[M];//用来判断是否是否放入背包
double zw;//当前放进背包总重量
double zv;//当前放进背包总价值
double bestv; //当前最优值
double bestx[M]; //当前最优解
double Bound(int i) //计算上界
{
int rp = 0;
while (i <= n) //计算剩余物品重量和
{
rp += v[i];
i++;
}
return zv + rp;
}
void Backtrack(int t) //回溯函数
{
if (t > n)//已经到达了叶子结点
{
for (int j = 1; j <= n; j++)
{
bestx[j] = x[j]; //记录编号
}
bestv = zv;//保留当前最优值
return;
}
if (zw + w[t] <= W)//如果满足条件,扩展并且搜索左子树
{
x[t] = 1;
zw += w[t];
zv += v[t];
Backtrack(t + 1);//回溯
zw -= w[t];
zv -= v[t];
}
if (Bound(t + 1) > bestv)//如果满足限界条件,搜索右子树
{
x[t] = 0;
Backtrack(t + 1);//回溯
}
}
//计算最优解
void Knapsack(double W, int n)
{
//初始化
zw = 0;
zv = 0;
bestv = 0;
double sumw = 0;
double sumv = 0;
for (int i = 1; i <= n; i++)
{
sumw += w[i];
sumv += v[i];
}
//如果背包够大都能放进去,那最大价值就是全部都进去
if (sumw <= W)
{
bestv = sumv;
cout << "放进背包的物品最大价值是:" << bestv << endl;
return;
}
Backtrack(1);//回溯
cout << "放入背包的物品最大价值是:" << bestv << endl;
cout << "放进背包的物品的序号是:";
for (int i = 1; i <= n; i++)
{
if (bestx[i] == 1)
{
cout << i << " ";
}
}
cout << endl;
}
int main()
{
cout << "请输入物品的个数:" ;
cin >> n;
cout << "请输入背包的重量W:";
cin >> W;
cout << "请依次输入每个物品的重量w,用空格分开: ";
for (int i = 1; i <= n; i++)
{
cin >> w[i];
}
cout << "请依次输入前面所输入的物品每个所对应的的价值v,用空格分开: ";
for (int i = 1; i <= n; i++)
{
cin >> v[i];
}
cout << endl;
Knapsack(W, n);
return 0;
}
结果:
- 心得体会
本次实验了解了用回溯法求解0-1背包问题的最优值和最优解,分别通过笔算和用代码实现了回溯法求解0-1背包的具体计算过程。
遇到的问题如下:
- 在实验中对于剪枝优化的代码不知道怎么去实现
根据深度优先遍历先去搜索左子树,并把所有左子树的最优值找到。通过函数调用,在当前节点的时候去计算后面右节点的所有后面层数的价值总数有没有超过已经计算过的最优值,如果在这一节点他的右子树后面的所有值加起来会比当前节点所有的左子树的最优值还大,那么就可以执行右子树的遍历否则直接剪掉右子树的遍历。通过规定上界函数解决剪枝问题,问题得到解决。
- 在求最优解时如何打印出最优解的位置选择?
在实验中我在计算每一层节点是否要选择的时候都是给X[t]数组来赋值0或1以用来表示是否选择该节点放进背包,当遍历到叶子节点时,我就会把这一条遍历线路上计算的最优值保存然后把每一层的节点的选择情况保存到另一个数组中,当后面有更好的最优值的时候,这些数据都会被覆盖替代。到最后通过for循环将取值为1的输出出来即可,下标的情况就是层数的情况,即可知道节点的选择情况如何了,问题得到解决。