【算法分析】实验 4. 回溯法求解0-1背包等问题
实验内容
本实验要求基于算法设计与分析的一般过程(即待求解问题的描述、算法设计、算法描述、算法正确性证明、算法分析、算法实现与测试),通过回溯法的在实际问题求解实践中,加深理解其基本原理和思想以及求解步骤。求解的问题为0-1背包。
作为挑战:可以考虑回溯法在其他问题(如最大团问题、旅行商、图的m着色问题)。
实验目的
- 理解回溯法的核心思想以及求解过程(确定解的形式及解空间组织,分析出搜索过程中的剪枝函数即约束函数与限界函数)。
- 掌握对几种解空间树(子集树、排列数、满m叉树)的回溯方法。
- 从算法分析与设计的角度,对0-1背包问题的基于回溯法求解有更进一步的理解。
实验结果
步骤1:描述与分析
给定n种物品和一个背包。物品\(i\)的重量是\(w_i\) ,其价值为\(v_i\),背包的容量为W。一种物品要不全部装入背包,要不不装入背包,不允许部分物品装入的情况。装入背包的物品的总重量不能超过背包的容量,在这种情况下,问如何选择转入背包的物品,使得装入背包的物品的总价值最大?需要采用回溯的方法进行问题的求解。
分析:
(1)问题的解空间:
将物品装入背包,有且仅有两个状态。第\(i\)种物品对应\((x_1,x_2,...,x_n)\),其中\(x_i\)可以取0或1,分别代表不放入背包和放入背包。解空间有\(2^n\)种可能解,也就是\(n\)个元素组成的集合的所有子集的个数。采用一颗满二叉树来讲解空间组织起来,解空间树的深度为问题的规模\(n\)。
(2)约束条件:
(3)限界条件:
0-1背包问题的可行解不止一个,而目标是找到总价值最大的可行解。因此需要设置限界条件来加速找出最优解的速度。如果当前是第t个物体,那么1-t物体的状态都已经被确定下来,剩下就是t+1~n物体的状态,采用贪心算法计算当前剩余物品所能产生的最大价值是否大于最优解,如果小于最优解,那么被剪枝掉。
步骤2:策略以及数据结构
采用回溯法进行问题的求解,也就是具有约束函数/限界条件的深度优先搜索算法。
采用回溯法特有框架:
回溯算法()
如果到达边界:
记录当前的结果,进行处理
如果没有到达边界:
如果满足限界条件:(左子树)
进行处理
进行下一层的递归求解
将处理回退到处理之前
如果不满足限界条件:(右子树)
进行下一层递归处理
步骤3
描述算法。希望采用源代码以外的形式,如伪代码或流程图等;
伪代码:
递归式回溯算法:
BACKTRACK-REC(t) //t为扩展结点在树中所处的层次
if t > n //已到叶子结点,输出结果
OUTPUT(x)
//检查扩展结点的每个分支。s(n,t)与e(n,t)分别为当前扩展结点处未搜索过
//的子树的起始编号和终止编号
else
for i from s(n,t) to e(n,t)
x[t] = h(i) // h[i]: 在当前扩展结点处 x[t]的第i个可选值
if CONSTRAINT(t) && BOUND(t) //约束函数与限界函数
BACKTRACK-REC(t+1) //进入t+1层搜索
迭代式回溯算法:
BACKTRACK-ITE()
t = 1 //t为扩展结点在树中所处的层次
while t > 0:
if s(n,t) <= e(n,t)
for i from s(n,t) to e(n,t)
do x[t] = h(i) //h[i]: 在当前扩展结点处x[t]的第i个可选值
if CONSTRINT(t) && BOUND(t) //满足约束限界条件
if t > n //已到叶子结点,输出结果
OUTPUT(x);
else
t ++ //前进到更深层搜索
else
t -- //回溯到上一层的活结点
步骤4
算法的正确性证明。需要这个环节,在理解的基础上对算法的正确性给予证明
回溯算法适用条件:多米诺性质
假设解向量是n维的,则下面的k满足:\(0<k<n ,P(x_1,x_2,x_3,…,x_{k+1})\)为解的部分向量可以推得\(P(x_1,x_2,x_3,…,x_k)\)也为解的部分向量
在0-1背包问题中,解空间为:\((x_1,x_2,...,x_n)\), 如果当前结果\(P_1 = (x_1,x_2,...,x_n)\)是最优解,那么\(P_2=(x_1,x_2,...,x_{n-1})\)的时候,也就是减少一个物品但不改变背包容量的时候,可以想到\(P_2\)依然是该问题的最优解。从子集树角度来看,也就是最后一层结点全部去掉后的结果,那么当前结果也是最优的。
步骤5
算法复杂性分析,包括时间复杂性和空间复杂性;
算法的复杂性分析:
时间复杂度:$$ T(n)=O(2n)+O(n2n)+O(nlog(n)) = O(n2^n)$$
空间复杂度:$$O(nlog(n))$$
步骤6
算法实现与测试。附上代码或以附件的形式提交,同时贴上算法运行结果截图;
# -*- coding: utf-8 -*-
"""
Created on Mon Oct 22 08:49:13 2018
@author: pprp
"""
BV=0 # best value
CW=0 # current weight
CV=0 # current value
BX=None # best x result
def output(x):
for i in x:
print(" ",i,end="")
print()
class node(object):
def __init__(self,v,w):
self.v = v
self.w = w
self.per = float(v)/float(w)
def Bound(t):
print("bound:",t)
LC = c-CW # left C
B = BV # best value
#sort
nodes = []
for i in range(n):
nodes.append(node(v[i],w[i]))
nodes.sort(key=lambda x:x.per,reverse=True)
# 装入背包
while t < n and w[t] <= LC:
LC -= w[t]
B += v[t]
t += 1
if t < n:
B += float(v[t])/float(w[t]) * LC
return B
def backtrack(t,n):
"""当前在第t层"""
print("current:",t)
global BV,CV,CW,x,BX
if t >= n:
if BV < CV:
BV=CV
BX=x[:]
else:
if CW+w[t] <= c: # 搜索左子树,约束条件
x[t]=True
CW += w[t]
CV += v[t]
backtrack(t+1,n)
CW -= w[t]
CV -= v[t]
if Bound(t) > BV: # 搜索右子树
x[t]=False
backtrack(t+1,n)
if __name__ == "__main__":
n=10
c=10
x=[False for i in range(n)]
w=[2,2,6,5,4,4,3,4,6,3]
v=[6,3,5,4,6,2,8,3,1,7]
backtrack(0,n)
print("Best Value :",BV)
print("Best Choice:",BX)
运行结果:
验证:6+3+8+7=24
实验总结
回溯法的思想:
能进则进,不进则换,不换则退.
回溯算法的框架:
以DFS的方式进行搜索,在搜索的过程中用剪枝条件(限界函数)避免无效搜索。约束函数,在扩展结点处剪去得不到可行解的子树;限界函数:在扩展结点处剪去得不到最优解的子树。
回溯算法求解问题的一般步骤:
1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
2 、确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
常用剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。
子集树、满m叉树、排列树区别:
子集树:从n个元素的集合S中找到满足某种性质的子集时,相应的解空间树就成为了子集树(典型问题:01背包问题)
满m叉树:所给问题中每一个元素均有m中选择,要求确定其中的一种选择,使得对这n个元素的选择结果组成的向量满足某种性质(经典问题:图的m着色问题)
排列树:从n个元素的排列树中找出满足某种性质的一个排列的时候,相应的解空间树称为排列树(经典问题:TSP问题,n皇后问题)