APP下载

求解0-1背包问题的多种算法策略的分析

2023-10-20文晓棠钟广玲

现代计算机 2023年15期
关键词:限界剪枝背包

陈 艳,文晓棠,钟广玲

(广州华商学院数据科学学院,广州 510520)

0 引言

0-1 背包问题是一个经典的组合优化问题,它涉及到在一个给定容量的背包中选择一些物品,使得这些物品的总价值最大。该问题常常被应用于资源分配、物流管理等领域,并且在计算机科学和数学中具有重要的理论价值。本文将采用深入浅出的方式,结合实例详细介绍如何使用动态规划、贪心算法以及分支限界法来解决0-1背包问题,并对各种方法的优缺点进行比较。

1 问题概述

0-1 背包问题描述为:有一个容量为C的背包,可以放入背包的物品有n种,每种物品的体积和价值分别为vi和pi。在不超过背包容量的前提下,怎样选择放入背包中的物品,以使得放入背包中的物品的价值和最大?需要注意的是,对于每种物品,只有两种选择,要么放入背包,要么不放入背包,因此称为0-1背包问题。

问题的数学模型如下:

输入:n个物品的集合O,每个物品有两个属性vi和pi,分别表示物品的体积和价值;背包容量为C。

输出:求解一个物品的子集S⊆O,使得。

2 动态规划

动态规划法与分治法类似,其基本思想都是将待求解问题分解成若干个子问题,然后从这些子问题的解得到原问题的解。动态规划法通过建立状态转移方程,也称递推式,来反映大问题和子问题之间的依赖关系,通过这种依赖关系来实现子问题的解组合成原问题的解[1]。

用动态规划法求解的问题一般具有两个特征:①最优子结构的性质。原问题的解可以通过子问题的解组合而成,这样的性质就称为最优子结构性质[2]。比如在分治法中,归并排序通过合并算法将两个子问题的解合并为原问题的解,因此分治法具有最优子结构的性质。比如在动态规划法中,求最大子数组问题的解是通过D[i]=D[i- 1]+X[i]或者D[i]=X[i]得到,即通过状态转移方程得到,因此动态规划法也具有最优子结构的性质。动态规划法的最优子结构的性质体现在状态转移方程上,问题的最优子结构性质的分析也是该问题是否能用动态规划求解的关键步骤。②重叠子问题的性质。一个大问题,通过分解后得到的子问题,通常存在公共的子问题,这样的性质称为重叠子问题的性质。比如斐波那契数列,求第5项需要通过第4 项和第3 项来得到,求第4 项需要通过第3 项和第2 项来得到,则求第5 项和求第4项都包含求第3项这个公共的子问题,故斐波那契数列具有重叠子问题性质。解决求解重叠子问题的效率问题,是动态规划算法的关键。

动态规划算法的基本思想的核心,就是解决重叠子问题重复计算的问题[3]。动态规划算法对分解出的每个子问题只计算一次,不管该子问题是否以后被用到,都将其结果保存到一张表中,从而避免每次遇到各个子问题时重新计算答案。基于这样的一种思想,相对于递归算法而言,动态规划法大大提高了问题的求解效率。应用动态规划算法的求解过程,实际上就是填表的过程,把相关表填完,问题的最优解就在表中的某个位置,可能是最后一个位置,也可能是中间的某个位置。动态规划算法求解问题的顺序去掉了递归算法的自顶向下分解的过程,只保留了自底向上不断求解子问题的过程,每个子问题的解都对应地存储在表中的某个单元格中。所以,动态规划法求解问题的基本步骤就是围绕着填什么表,怎么填,填完之后怎么在表中找最优解的过程来分析的。

动态规划法求解问题的基本步骤分为四步:

第一步:子问题的定义和表示。

通过分析问题的结构特点来定义子问题,找到合适的状态来描述子问题,并通过定义的状态来表示原问题。主要任务是确定用来存放子问题解的表的样式,如一维表还是二维表或者三维表,等等,并确定原问题的表示。

第二步:状态转移方程(递推式)的建立。

分析子问题之间解的依赖关系,以此来建立状态转移方程。因此此步的关键问题是证明问题的最优子结构性质,问题的最优子结构性质得以证明,则问题解之间的依赖关系也就随之得到了。动态规划法求解问题,最关键的就是建立状态转移方程,这一步骤解决了,后面的问题水到渠成。

第三步:填表顺序的确定。

在状态转移方程中,根据问题和子问题之间的依赖关系来确定填表顺序,即子问题的求解顺序。具体的确定方法可以将相关的子问题在表中的位置标识出来,通过待求解的子问题和求解依赖的子问题在表中的位置关系来分析填表的顺序。有的问题填表的顺序为从左到右,有的为从右到左,有的从上到下,不同的问题根据其特征不同填表顺序也不同。填表顺序确定后,就能在此步确定原问题的解在表中的位置,可能填的最后一个元素即为原问题的解,也可能解存在于表中的某一个需要通过一定策略得到的位置,比如表中元素的最大值即为原问题的解。

第四步:最优方案的追踪。

在第三步进行的填表过程中,通常是需要通过一定的决策来填表,比如选最大值或最小值等。做的决策是最优解对应的最优方案获取的直接依据,因此,在对子问题的解填表的过程中,可以顺便把相应的决策记录下来,如也用一张表来记录。根据决策表,采用回溯的方式来寻找最优解对应的最优方案。这一步,根据问题的具体要求,如果只需要求解最优解,不需要求出最优方案,则可以省略。

动态规划是解决0-1 背包问题的经典算法,它通过构建问题的状态转移方程(递推式),从而求解出最优解。采用二维数组DP 来存储每个子问题的最优解,其中第i行j列DP[i,j] 表示:对前i个物品,容量为j的背包能够装下的最大价值。对于每个物品,考虑放或者不放两种情况,即可推导出状态转移方程,由此推算出问题的最优解。按照此思路,根据动态规划算法求解问题的基本步骤,设计如下:

(1)子问题的定义和表示。

原问题的子问题定义为:对前i个物品,当背包容量为j时,在不超过背包容量限制的前提下,使得放入背包中的物品的价值最大。此问题和原问题是性质相同,规模变小的相同问题,故为原问题的子问题。对子问题,表示如下:

定义DP[i,j]:在前i个商品中做出选择,背包容量为j时放入背包中商品的最大总价值。

(2)状态转移方程(递推式)的建立。

状态转移方程意味着,在考虑第i个物品时,我们可以选择放或者不放,分别求两种情况的最优解,然后再看哪种情况求得的价值最大,则其就是问题的解。如果选择放第i个物品,那么此时背包的容量就减少了vi,并且能够获得的价值增加了pi,此时只要把前i-1 个物品在背包容量为j-vi时的最优解求出来,加上i件物品的价值贡献,就可得到此情况的最优解,即求DP[i- 1,j-vi]+pi。如果选择不放第i个物品,那么此时背包的容量依然是j,则此问题转换为求解前i-1 个物品,背包容量为j时的子问题的最优解问题,即求DP[i- 1,j]。两种情况取大值即为原问题的解。

根据递推关系的分析,DP 实际上是一张二维表,表规模为n+ 1行、C+ 1列。第0行表示没有物品的情况,第0 列表示没有背包的情况,这两类情况能放入背包中的物品的总价值和都为0,故可以都初始化为0,如图1所示。

图1 DP表的样式

(3)填表顺序的确定。

根据上述分析,求解0-1背包问题实际上就是要完成上述DP 的计算问题。根据状态转移方程,在表中分别表示出DP[i,j]和求解它依赖的两个子问题DP[i- 1,j]和DP[i- 1,j-vi],如图2所示。

图2 子问题之间的位置关系

根据图中子问题间的位置关系可知,要求DP[i,j],则需先求出其上一行的DP[i- 1,j]和DP[i- 1,j-vi],由此不难看出,DP 的整个计算顺序为从左到右,从上到下,一行一行地填,且最后一个元素DP[n,C]为原问题的解,如图3所示。

图3 原问题的解的位置

(4)最优方案的追踪。

根据状态转移方程可知,求DP[i,j]的过程实际上就是对i号物品选还是不选进行决策的过程,因此在填每个DP[i,j]时,把做出的决策记录下来,可以依据此决策推出最优解对应的最优方案,见下式:

在Dec 表中倒叙判断是否选择商品,如图4所示。

图4 Dec表

假设有个背包,容量C= 7,有5 件可选择的物品,各物品的价值和体积见表1。

表1 物品清单

上述问题最优解为22,最优方案为S={5,4,1},即选择5 号、4 号和1 号物品放入背包中,此时放入背包中的物品的总体积为7。

按上述分析过程得到本例子的DP 表如图5所示,从DP表中可知原问题的最优解为22。

图5 案例DP

最优方案的追踪过程如图6所示,得到最优解22对应的最优方案为{5,4,1},即选择5号、4号和1号商品放入背包中。

图6 最优方案的追踪

根据上述分析,设计算法DPknapsack(n,p,v,C)的伪代码如下:

该算法用两层循环来实现,循环的规模分别为n和C,故算法的复杂度为O(n*C)。

3 回溯法

回溯法是一种基于深度优先搜索的算法,也称为回溯搜索法,它是一种搜索的方式,常用于解决组合问题、子集问题、棋盘问题等。其核心思想是通过试错的搜索方式,逐步构造可行解,并在发现不符合要求的情况下回溯到上一步,重新选择其他可能的路径[4]。

通常,回溯法可以使用递归函数来实现,但它不是单纯的递归算法。递归函数中包含两个关键部分:一个是生成可行解的过程,即对当前状态进行扩展;另一个是判断该状态是否满足条件,对应分支是否有进行下去的意义,即剪枝的过程[5]。具体步骤如下:

第一步:定义解空间,用树来表示解空间。解空间表示所有可行的解的集合。定义解空间,并确定搜索解空间的结构树,即解空间树。

第二步:定义约束函数和限界函数。定义约束函数和限界函数的目的是通过这两种策略避免无效搜索,提高回溯法的搜索效率。约束函数的作用是在扩展结点处减去不满足约束的子树,限界函数的作用是剪去得不到最优解的子树。因此这两类函数都称为剪枝函数。

第三步:以深度优先的方式搜索解空间树。在搜索的过程中通过两个剪枝函数来避免无效搜索。对解空间树的每个节点上的搜索具体过程为:当前节点为活结点,同时也成为当前的扩展结点。在当前扩展结点处,搜索向纵深方向扩展出一个新的结点来,并判断约束条件或限界函数。如果此处不满足约束条件或者限界函数,则剪枝,即此结点称为死结点。此时,应往回移动(回溯)至最近的活结点处,并使这个结点成为当前的扩展结点。回溯法以这种方式递归地在解空间树中搜索,直至找到所要求的解或解空间树中已无活结点为止。

假设有个背包,容量C= 30,有3件可选择的物品,各物品的价值和体积以及按照单位体积的价值比排好序,见表2。

表2 物品清单

按照上述求解步骤,对此问题的求解过程如下:

(1)定义解空间,用树来表示解空间。

该问题解空间为{(0,0,0),(0,1,0),(0,0,1),(1,0,0),(0,1,1),(1,0,1),(1,1,0),(1,1,1)},解空间树如图7所示。

图7 解空间树

(2)定义约束函数和限界函数。

定义约束函数c(i)=cv(i-1)+vi,cv(i-1)表示前i-1个物品中已经放入背包的物品的体积,则c(i)表示当前放入背包中的物品的体积,任何结点上必须满足cv(i)≤C的约束条件,否则剪枝。

定义限界函数B(i)=cp(i-1)+r(i),cp(i-1)表示已经放入背包中物品的价值,r(i)表示背包剩余容量可以存放的理想最大价值。r(i)可以通过贪心法的思想来求解,将物品按价值体积比的降序排序,优先选择比值高的物品,直到把整个背包撑满的理想状态可以存放的最大价值,则B(i)表示当前状态下将背包撑满能放入物品的理想最大价值,如果用bp表示当前记录的最大组合的价值和,则如果B(i)≤bp,此分支没有继续下去的意义,故而剪枝。

(3)以深度优先的方式搜索解空间树。

根据上述的分析,定义cv表示当前放入背包中的物品的体积,cp表示当前放入背包中物品的价值和,bp表示当前记录的最优组合的价值和,r表示还未做出选择的剩余物品的价值和。得到问题的带剪枝的以深度优先方式搜索的解空间树,如图8所示。

图8 带剪枝的解空间树

从图8 可知,第3 层结点(1, 1, 0)、结点(0,0,0)和第4层结点(1,0,1)都因为约束条件或者限界函数执行了剪枝,算法在执行的过程中,执行的分支数量大幅下降,由此算法的复杂度也得到大幅提升。完整的搜索过程为A→B→E→K→C→F→L。

算法BTknapsack(n,p,v,C)伪代码如下:

算法backtrack(i,cv,cp)伪代码如下:

回溯部分通过调用backtrack(i,cv,cp)函数来实现。该函数包含三个参数:当前考虑到第几个物品(即状态),已选择的物品总体积和总价值。如果所有物品都已经考虑过,或者当前已选物品体积达到背包容量,则更新最大价值并返回。如果cp+r小于等于bp,说明把背包撑满的理想状态能装入的最大价值已经不大于当前的最大价值,则该分支没有继续下去的必要,因此直接返回,即剪枝。否则,对于第i个物品,可以有两种选择:选或不选。如果不选第i个物品,则直接递归调用backtrack(i+1,cv,cp);如果选第i个物品,则需要判断当前已选物品总体积是否小于等于背包容量,如果是,则递归调用backtrack(i+1,cv+v[i],cp+p[i])。

4 分支限界法

分支限界法是一种求解最优化问题的常用算法,它基于将问题空间划分为一个个子集,每个子集对应一个可行解,通过比较不同可行解的目标函数值来逐步缩小搜索范围,最终找到全局最优解。分支限界法和回溯法类似,求解策略都是在问题的解空间上搜索问题解。但分支限界法与回溯法有着不同的求解目标。回溯法的求解目标是在约束条件的限制下,把解空间树中满足条件的所有解都找出来,而分支限界法的求解目标则是在约束条件的限制下,找到满足条件的一个解,即在某种意义下的一个最优解[5]。

由于求解目标的差异,导致分支限界法与回溯法在解空间树中搜索问题解的方式也不相同。由上文可知回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。按照广度优先的搜索策略,分支限界法的搜索策略,每个活结点只有一次机会成为扩展结点,一旦成为扩展结点,就一次性生成其所有的儿子结点(分支)。这些儿子结点中,不可行解或者非最优解的儿子结点被舍弃(剪枝),其余儿子结点加入活结点表(队列)中,然后再从当前的活结点表中选择下一个扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。为了有效地选择下一扩展结点,以加速搜索的进程,在每一活结点处,计算一个函数值(限界函数),并根据函数值从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间上有最优解的分支推进,以便尽快地找出一个最优解。这种方法称为分支限界法。

从活结点表中选择下一扩展结点的不同策略导致不同的分支限界法。最常见的有以下两种方式。

(1)队列式(FIFO)分支限界法。

队列式分支限界法将活结点表组织成一个队列,并按队列的先进先出原则选取下一个结点为当前扩展结点。

(2)优先队列式分支限界法。

优先队列式的分支限界法将活结点表组织成一个优先队列,并按优先队列中规定的结点优先级选取优先级最高的下一个结点为当前扩展结点。

优先队列中的结点优先级常用一个表示该结点耗费或收益的函数来表示,即如果扩展该结点,通过该函数来计算会带来多大的效益,以此来决定结点的优先级。函数值的确定通常采用堆来实现。

和回溯法类似,分支限界法求解问题的基本步骤如下:

第一步:定义解空间,用树来表示解空间。解空间表示所有可行的解的集合,集合中一定包含问题的最优解。定义解空间,并确定搜索解空间的结构树,即解空间树。

第二步:定义约束函数和限界函数,统称为剪枝函数。定义约束函数和限界函数的目的是通过这两种策略来决定在扩展中哪些儿子节点被舍弃(剪枝),不被舍弃的儿子结点加入到队列中。

第三步:以广度优先的方式搜索解空间树。在搜索的过程中和回溯法一样,通过两个剪枝函数来避免无效搜索。如果采用队列式(FIFO)分支限界法,则不被舍弃的儿子结点依次加入队列,以先进先出的顺序对结点进行扩展。如果采用优先队列式分支限界法,则按照某种策略求解每个儿子结点的优先级对队列进行排序,每次扩展队列中优先级最高的结点。

分支限界法也是一种常见的求解0-1背包问题的算法,它通过将问题分解成子问题并逐步减小搜索空间来寻找最优解。具体地,可以先将所有物品按照单位重量的价值从大到小排序,然后从排好序的物品列表中依次选择物品放入背包中。同时,维护一个上界和一个下界,以便剪枝减少搜索空间。继续以表2中的物品清单为例来分析分支限界法求解0-1背包问题,具体步骤如下:

(1)定义解空间,用树来表示解空间。

此步骤和回溯法完全相同,此处不再赘述。

(2)定义约束函数和限界函数,统称为剪枝函数。

此步骤和回溯法完全相同,此处不再赘述。

(3)以广度优先的方式搜索解空间树。

定义约束函数c(i)=cv(i-1)+vi,定义限界函数B(i)=cp(i-1)+r(i),两个函数的定义同回溯法,此处不再赘述。限界函数B(i)的意义除了作为剪枝的依据,B(i)的值还作为结点的优先级的值来决定在优先队列中是否下一次扩展该结点。如果优先级最高,则扩展此结点,反之以优先级的降序存放在优先队列中待扩展。通过队列中结点优先级的设置,可以使得搜索的过程朝着目标快速地逼近,简化了搜索过程,提高了效率。搜索过程的总方式是广度优先,考虑B(i)的值为优先级作为下一次扩展结点选取的依据,因此,搜索的过程是基于广度优先但又不完全是传统意义的广度优先搜索法。在图8 中,优先队列的分支限界法的搜索过程为A→B→C→E→K→F→L。优先队列的搜索算法,以从当前节点出发最理想的价值为优先级,使得搜索过程能以最快的速度找到最优解,后面结点的扩展因当前的bp值而发生剪枝,从而提升了算法的执行效率。

根据上述分析,设计算法BBknapsack(n,p,v,C)的伪代码如下:

通过wt<=C约束条件来实现左枝的剪枝,通过限界up>=bestp来实现右枝的剪枝,每个活结点扩展出来的新结点,根据其优先级up,加入到优先队列中,每次都选择优先级最高的结点来进行扩展。

5 算法对比实验

为了比较三种算法的性能,先用简单的测试数据,令v={2,3,4,5},p={3,4,5,6}分别对C的值取8(最差情况)、取10(平均情况)和取12(最好情况)进行测试,测试结果表明,三种情况执行效率最高的为回溯法,执行时间基本都在0.03 ms 左右,其次是动态规划法,执行时间在0.3 ms 左右,性能最差的为分支限界法,执行时间在1.5 ms左右。

为了比较大数据下三种算法的性能,分两类数据进行测试。一类固定n的大小为100,随机生成100 个物品,每个物品的体积和价值在1~100 之间取值。对于从30~3000000 不同规模的背包容量,分别使用动态规划法、回溯法和分支限界法来求解,并计算求解时间,实验结果见表3。另一类固定背包容量的大小为300,随机生成n的规模为10~1000000 个物品,每个物品的体积和价值在1~100 之间取值。对于不同规模的物品数量,分别用三种算法求解,并计算求解时间,实验结果见表4。

表3 物品规模固定实验结果

表4 背包容量固定实验结果

对上述两表生成对比图形,如图9 和图10所示。

图9 不同背包容量性能比较

图10 不同物品数量性能比较

结合上述图和表可以看出,三种算法在求解0-1 背包问题时的性能差异较大。具体来说,回溯法比较稳定,不论物品数量和背包容量发生什么变化,其求解问题的时间基本都在3 ms 以内。回溯法的执行时间对背包容量的增加不敏感,对物品数量的增加执行时间有小幅上涨。而动态规划法,固定物品的数量,不同的背包容量,求解问题的时间从0.54 ms 增长到1542 ms,随着背包容量的增加,当增加到30000时,算法性能直线下降。固定背包容量大小,不同的物品数量规模,动态规划的性能表现和固定物品数量类似。原因是动态规划的实质是填规模为n×C的表,算法执行时间和填表的规模有关。分支限界法的性能介于动态规划和回溯法之间,和回溯法类似,算法对背包容量的变化不敏感,对物品数量的增加敏感,且和回溯法相比,随着物品数量的增加,分支限界法的执行时间增长明显。分支限界法的性能和回溯法比不具优势的主要原因在于应用优先队列存储结点,优先队列的应用有较大的时间消耗。

总体而言,性能上回溯法具有绝对优势,当问题规模(物品数量和背包容量)较小时,动态规划优于分支限界;当问题规模较大时,分支限界优势明显。究其原因,回溯法和分支限界法虽然本质是穷举,但当物品数量或背包容量很大时,通过剪枝的优化,大大减少了子问题执行的数量,算法性能得到极大提高。但反观动态规划算法,不论问题规模和背包容量多大,都要将其所有子问题求解,当问题规模小、背包容量小时,子问题的数量不大,整个问题求解时间短,但当问题规模和背包容量达到一定规模时,待求解的子问题数量也同时达到相当的规模,求解这些子问题所需要的时间自然也就具有较大规模。因此,在实际应用中,对规模较小的问题,推荐使用动态规划法,当问题规模较大或者背包容量较大时,则推荐使用回溯法来求解。当采用有效的排序算法和优先队列时,分支限界法也是一个不错的选择。

6 结语

综上所述,动态规划、回溯法和分支限界法是三种常见的求解0-1背包问题的方法。动态规划算法具有良好的理论保证,当问题规模不大时可以在较短的时间内求解出全局最优解。回溯法和分支限界法是一种基于搜索的高效算法,因其本质是穷举,且是带剪枝的优化穷举,故可以在合理的时间内找到全局最优解。在实际应用中,我们需要根据问题特点和求解需求来灵活选择算法,并尽可能利用并行化和分布式计算等技术来提高求解效率。

猜你喜欢

限界剪枝背包
人到晚年宜“剪枝”
基于YOLOv4-Tiny模型剪枝算法
大山里的“背包书记”
一包装天下 精嘉Alta锐达Sky51D背包体验
鼓鼓的背包
创意西瓜背包
剪枝
限界检查器设置方案的探讨
地铁隧道施工偏差限界检测软件开发与应用
一种面向不平衡数据分类的组合剪枝方法