回溯法
回溯法是一种非常有效的方法,有“通用的解题法”之称。它有点像穷举法,但是更带有跳跃性和系统性,他可以系统性的搜索一个问题的所有的解和任一解。回溯法采用的是深度优先策略。
回溯法在确定了解空间后,从根结点出发,以深度优先的方式搜索整个解空间,此时根结点成为一个活结点,并且成为当前的扩展结点。从扩展结点向纵向搜索新的结点,当算法搜索到了解空间数的任一结点,先判断该结点是否肯定不包含问题的解(是否还能或者还有必要继续往下搜索),如果不包含问题的解,就逐层回溯;否则,进入子树,继续按照深度优先的策略进行搜索。当回溯到根结点时,说明搜索结束了,此时已经得到了一系列的解,根据需要选择其中的一个或者多个解即可。
回溯法解决问题一般分为三个步骤;
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先的方式搜索解空间。
回溯法的典型实例——0-1背包问题
为了方便理解回溯法运算的流程,以0-1背包问题为例进行分析;
问题:给定一个载重量为W,n个物品,物品的重量为Wi,价值为Vi,1<=i<=n,要求:把物品装入背包,并使包内物品价值最大。
首先考虑贪心法,为了得到最大的价值,将所有物品按照单位价值(Vi/Wi)降序排列(例如采用希尔排序,时间复杂度为),在放入物品时优先考虑单位价值更高的物品。在搜索到空间树中的某个结点P时,已经确定了P及其前面的结点取值,进而判断从P继续扩展下去是否获得更大的价值,如果不能,该结点无需扩展,可以进行回溯了。下面的函数结合了贪心法判断从某一点扩展开去可能获得的最大的价值。
int Bound(int *Values, int *Weights,int n,int maxWeight,int num,int current_Weight,int current_profit) //--------------num为已选中的最后一个物品的编号,current_weight为现在的重量,current_profit为现有价值 { int i = num + 1; for (; i < n; i++) { if (current_Weight + Weights[i] < maxWeight) { current_profit += Values[i]; current_Weight += Weights[i]; } else { current_profit += (Values[i] / Weights[i])*(maxWeight - current_Weight); //计算是否有可能获得更大的价值,贪心策略; current_Weight = maxWeight; return current_profit; } } return current_profit; }
下面的代码实现在解空间进行搜索的过程;
int *Knapsack(int *Values,int *Weights,int n,int maxWeight) { int *X = new int[n]; int *Y = new int[n]; int Weight = 0; int Profit = 0; int current_weight=0, current_profit=0; int i = 0; while (1) { while (i<n&&t_weight + Weights[i] <= maxWeight) { X[i] = SELECT; current_profit += Values[i]; current_weight += Weights[i]; i++; } //---上面的循环中,如果是由于i=n结束的,那么说明深度搜索已经搜索到了最底层 if (i >= n) { Weight = current_weight; Profit = current_profit; i = n; for (int j = 0; j < n; j++) //------------把数组X挪给Y; { Y[j] = X[j]; } } //否则就是由于第i个物品在当前情况下无法放入背包 else { X[i] = UNSELECT; } while (Bound(Values, Weights, n, maxWeight, i, current_weight, current_profit) <= Profit)//如果不可能获得更大的价值,那么这个点就不需要进行扩展了; { while (i != 0 && X[i] != SELECT)//进行回溯 { i--; } if (i == 0) //当回溯到i=0时候,所有情况都遍历了 { return Y; } X[i] = UNSELECT; current_profit -= Values[i]; current_weight -= Weights[i]; } i++; } }
假设n=8,W=110;物品的价值和重量如下表(已按照单位价值排序);
物品 i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Vi | 11 | 21 | 31 | 33 | 43 | 53 | 55 | 65 |
Wi | 1 | 11 | 21 | 23 | 33 | 43 | 45 | 55 |
那么问题的解空间就很明显了就是2的8次方个种问题的解的组合{(0,0,0,0,0,0,0,0)......(1,1,1,1,1,1,1,1)}。
使用二叉树构建解空间,这里解空间的一个结点共有两个候选值0、1 。 0代表不放进背包中(右子树), 1代表放进背包中(左子树),如下图所示;
采用深度优先策略进行搜索,搜索的过程如下所示;
搜索得到的最终的结果就是159;
搜索过程分析(可以对照代码阅读)
从第一个点开始向下扩展,扩展到将第四个点放入放入后,此时背包已经放入重量为56,价值为96。然后继续扩展,将第五个物品放入重量为89,价值为139。此时第六个物品无法继续放入,但是i=5的,也就是没有到达最后一个点,此时调用Bound()函数算出可能获得的最大的价值为164.66,那么继续向下扩展,但是无法放入物品了,继续调用Bound()判断是否向下扩展,直到最后一个物品也无法放入,确定此时搜索得到的可以获得的最大的价值为139,保存物品放入的选择。
然后开始回溯,回溯遇到第一个放入背包的物品对应的节点,将其取0(把物品拿出背包,进入右子树)。此时比较背包剩余重量,发现第六个物品可以放入背包,放入后背包重量为99,价值为149。但是,第七个物品无法继续放入,那么调用Bound(),发现可能获得162的价值,大于139,继续向下扩展,直到最后一个物品也无法放入,确定此时搜索得到的可以获得的最大的价值为149>139,更新可获得的价值,并且保存物品放入的选择。
继续回溯,直到回溯到根结点。此时的最大价值所对应的物品放入选择就是所求的解。
算法复杂度分析
在最坏的情况下,所搜索的结果是一个满二叉树,此时相当于采用的就是穷举法,时间复杂度为,而每次决定是否要讲n个物品放入背包都要进行比较,这一步的时间复杂度为n,所以最坏情况下时间复杂度为。
已完。。。。