本实验要求基于算法设计与分析的通常过程(即待求解问题的描述、算法设计、算法描述、算法正确性证实、算法分析、算法实现与测试),经过回溯法的在实际问题求解实践中,加深理解其基本原理和思想以及求解步骤。求解的问题为0-1背包。python
做为挑战:能够考虑回溯法在其余问题(如最大团问题、旅行商、图的m着色问题)。c++
给定n种物品和一个背包。物品\(i\)的重量是\(w_i\) ,其价值为\(v_i\),背包的容量为W。一种物品要不所有装入背包,要不不装入背包,不容许部分物品装入的状况。装入背包的物品的总重量不能超过背包的容量,在这种状况下,问如何选择转入背包的物品,使得装入背包的物品的总价值最大?须要采用回溯的方法进行问题的求解。算法
分析:数据结构
(1)问题的解空间:app
将物品装入背包,有且仅有两个状态。第\(i\)种物品对应\((x_1,x_2,...,x_n)\),其中\(x_i\)能够取0或1,分别表明不放入背包和放入背包。解空间有\(2^n\)种可能解,也就是\(n\)个元素组成的集合的全部子集的个数。采用一颗满二叉树来说解空间组织起来,解空间树的深度为问题的规模\(n\)。框架
(2)约束条件:
\[ \sum_{i=1}^nw_ix_i \le W \]
(3)限界条件:函数
0-1背包问题的可行解不止一个,而目标是找到总价值最大的可行解。所以须要设置限界条件来加速找出最优解的速度。若是当前是第t个物体,那么1-t物体的状态都已经被肯定下来,剩下就是t+1~n物体的状态,采用贪心算法计算当前剩余物品所能产生的最大价值是否大于最优解,若是小于最优解,那么被剪枝掉。测试
采用回溯法进行问题的求解,也就是具备约束函数/限界条件的深度优先搜索算法。spa
采用回溯法特有框架:
回溯算法() 若是到达边界: 记录当前的结果,进行处理 若是没有到达边界: 若是知足限界条件:(左子树) 进行处理 进行下一层的递归求解 将处理回退处处理以前 若是不知足限界条件:(右子树) 进行下一层递归处理
描述算法。但愿采用源代码之外的形式,如伪代码或流程图等;
伪代码:
递归式回溯算法:
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 -- //回溯到上一层的活结点
算法的正确性证实。须要这个环节,在理解的基础上对算法的正确性给予证实
回溯算法适用条件:多米诺性质
假设解向量是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\)依然是该问题的最优解。从子集树角度来看,也就是最后一层结点所有去掉后的结果,那么当前结果也是最优的。
算法复杂性分析,包括时间复杂性和空间复杂性;
算法的复杂性分析:
时间复杂度:\[ T(n)=O(2^n)+O(n2^n)+O(nlog(n)) = O(n2^n)\]
空间复杂度:\[O(nlog(n))\]
算法实现与测试。附上代码或以附件的形式提交,同时贴上算法运行结果截图;
# -*- 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的方式进行搜索,在搜索的过程当中用剪枝条件(限界函数)避免无效搜索。约束函数,在扩展结点处剪去得不到可行解的子树;限界函数:在扩展结点处剪去得不到最优解的子树。
回溯算法求解问题的通常步骤:
一、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
2 、肯定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
3 、以深度优先的方式搜索解空间,而且在搜索过程当中用剪枝函数避免无效搜索。
经常使用剪枝函数:
用约束函数在扩展结点处剪去不知足约束的子树;
用限界函数剪去得不到最优解的子树。
子集树、满m叉树、排列树区别:
子集树:从n个元素的集合S中找到知足某种性质的子集时,相应的解空间树就成为了子集树(典型问题:01背包问题)
满m叉树:所给问题中每个元素均有m中选择,要求肯定其中的一种选择,使得对这n个元素的选择结果组成的向量知足某种性质(经典问题:图的m着色问题)
排列树:从n个元素的排列树中找出知足某种性质的一个排列的时候,相应的解空间树称为排列树(经典问题:TSP问题,n皇后问题)