0/1背包问题-----回溯法求解

本文详细介绍了如何使用回溯法解决0/1背包问题,通过实例展示了回溯法的思路,包括问题描述、解空间树的构建、回溯过程,并分别用递归和迭代两种方式给出了具体的求解算法实现。最后讨论了如何输出最佳的背包装入情况。
摘要由CSDN通过智能技术生成

问题描述

有n个物品和一个容量为c的背包,从n个物品中选取装包的物品。物品i的重量为w[i],价值为p[i]。一个可行的背包装载是指,装包的物品总重量不超过背包的重量。一个最佳背包装载是指,物品总价值最高的可行的背包装载。
我们要求出x[i]的值。x[i] == 1表示物品i装入背包,x[i] == 0表示物品i没有装入背包。

问题的公式描述是:

//总价值最高的可能的背包装载,x[i]只能等于0或者1
max{p[1] * x[1] + p[2] * x[2] + ... + p[i] * x[i] + ... + p[n] * x[n]}

约束条件

//装入的所有物品的重量之和不大于背包容量
size_t totalWeight = 0;
for(size_t i = 1; i <= n; ++i)
{
    totalWeight += w[i] * x[i]; //x[i] = 0 || x[i] = 1
}
if(totalWeight <= c)
    //约束条件成立
else
    //约束条件不成立

回溯法

顾名思义,回溯法一个很显著的特征就是回溯,即在处理完某种情况或出现不能继续进行的情况时,要退回到之前的某个“交叉口”,处理另一种可能。
通常,回溯法会定义一个解空间,这个解空间通常是以图或树的形式呈现出来的。背包问题的解空间是树的形式,属于深度优先搜索。

问题分析

考察这样一个0/1背包问题: 总共4个物品,重量分别是w[1:4] = {8, 6, 2, 3},价值分别是p[1:4] = {8, 6, 2, 3},规定背包容量为12(即可以容纳的最大重量为12),求出获得最大价值的解情况。

回溯法首先要做的就是根据已知条件建立解空间树。
这里写图片描述
如图为根据4个物品的重量建立的解空间树,根节点A没有任何意义,每个节点的高度代表它是第几个节点(A节点高度为0)。对于一个节点Z,它的高度是H,则Z节点表示的就是物品H。左孩子表示装入物品H+1(权值是物品H+1的重量),右孩子表示不装入物品H+1(权值是0)。
可以理解为Z就是一个“交叉口”,向Z左边走是一种情况(即物品H+1装入背包的情况),向Z右边走是另一种情况(即物品H+1没有装入背包的情况)。
另外,如果Z的权值不是0,就证明Z的父节点的左孩子,表示物品H已经装入背包。

对于本题而言,起始位置在A点,当前可用容量为12.
1.因为12>8,所以可以向左孩子节点移动,此时位于B点,可用容量变为4,高度为1,表示第一个物品装入背包。
2.考虑B的左右孩子节点,因为当前可用容量不足以装入物品2,所以不能够移向D点,故移动到E点,可用容量仍为4,高度为2,表示第二个物品没有装入背包。
3.考虑E的左右节点J和K,因为当前可用容量可以装入物品3,所以可以移动到J点,可用容量变为2,高度为3,表示第三个物品装入背包。
4.考虑J的左右孩子节点,因为2<3,不足以装入物品4,所以只能够移动到J的右孩子结点。可用容量仍为2,高度为4,表示第四个物品没有装入背包。
5.到达叶子节点,这种情况下背包装入情况是[1,0,1,0],获得的总价值为10,记录此时的叶子节点。
6.开始向上回溯,即回到此时节点的父节点处,回到J点。可用容量为2,高度为3.
7.因为J的右孩子已经考虑过了,所以继续向上回溯,回到J的父节点E点。可用容量要加上J的重量,变为4,高度为2。这时回到了步骤3的交叉口(称为回溯)。
8.因为刚才移动到E的左孩子节点,所以回溯回来后移动到E的右孩子节点K。此时,可用容量不变,仍为4,高度为3。表示第三个物品没有装入背包。
9.考虑K的左右孩子节点,因为4>3,所以可以移动到K的左孩子节点,当前可用容量变为1,高度为4,表示第四个物品被装入背包。
10.到达叶子结点,这种情况下背包装入情况是[1,0,0,1],获得的总价值是11大于10,所以更新最优解的叶子节点。
11.继续向上回溯,直到处理完所有情况。

以上就是回溯法的大体思路,接下来会分别用递归和迭代两种方法求解。

递归求解

递归求解相对简单,不过要对回溯有比较好的理解。

为了减少递归调用传参的代价,可以把大部分的变量作为全局变量使用。

int *Weight; //Weight[i]表示物品i的重量
int *Profit; //Profit[i]表示物品i的价值量
int n; //物品个数

首先考虑如何构建解空间树,
一种方法是以树节点指针的形式描述,但是回溯起来需要另外添加父节点指针。
另一种方法是使用一维数组表示解空间树,对于某个节点Z,它在数组中的索引是index,则Z的左孩子索引是index * 2,Z的右孩子索引是index * 2 + 1,Z的父节点索引是index / 2。(这种描述方法和大根堆,小根堆一样)

这里采用一维数组描述解空间树,
1.需要事先算出树中节点个数。因为已知物品数量n,并且树中每一层代表一个物品,所以树的高度是n+1(算上根节点A)。利用等比数列求和公式可以求出节点个数为2的n+1次方-1。
2.初始化解空间树的一维数组。因为每一层的节点数量和该层的高度有关,所以可以事先算出这一层第一个节点的索引和最后一个节点的索引。然后一个赋值成Weight[i],一个赋值成0,每次i += 2。

初始化解空间树的代码如下:

//节点个数为2的n+1次方-1
size_t pNodeSize = 1;
for(size_t i = 1; i <= n+1; ++i)
    pNodeSize *= 2;
pNodeSize--;

//根节点的索引为1,权值为0
int *pTree = new int[pNodeSize+1];
pTree[1] = 0;

for(size_t pHeight = 1; pHeight <= n; ++pHeight)
{
    //高度为pHeight的第一个节点的索引为2的pHeight次方
    size_t pStartNode = 1;
    for(size_t i = 1; i <= pHeight; ++i)
        pStartNode *= 2;

    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值