1.问题的引入
要求找到一组解,或要求找到一个满足某些限制的最优解。通过彻底的搜索方法来解决:彻底的搜索,需要进行大量的比较、舍弃、运算时间为代价。因此,用穷举法解某些实际问题是不现实的,彻底搜索的运算量很大,有时大到计算机承受不了的程度。使用回溯法可以大大减少实际的搜索。例如,迷宫问题,八皇后问题,骑士周游世界问题。 关键是要找到回溯的条件。算法思想:通过对问题的分析,找出一个解决问题的线索,然后沿着这个线索往前试探,若试探成功,就得到解,若试探失败,就逐步往回退,换别的路线再往前试探。实际上是广度与深度搜索结合的搜索,深度搜索过程中碰到条件不满足,则退回上一层,在每一层上也进行全面的搜索。
2.回溯法的基本思想
回溯法是带优化的穷举法。回溯法的基本思想:在一棵含有问题全部可能解的状态空间树上进行深度优先搜索,解为叶子结点。在回溯法中,并不是先构造出整棵状态空间树,再进行搜索,而是在搜索过程中逐步构造出状态空间树,即边搜索,边构造。回溯法在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。
(1)如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。
(2)否则,进入该子树,继续按深度优先的策略进行搜索。
回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。
回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。
3.基本概念
实例:
0-1背包问题
n=3,C=30, w={16, 15, 15}, v={45, 25, 25}
开始时,Cr=C=30,V=0,A为唯一活结点,也是当前扩展结点;扩展A,先到达B结点;Cr=Cr-w1=14,V=V+v1=45;此时A、B为活结点,B成为当前扩展结点;扩展B,先到达C;Cr<w2,C导致一个不可行解,回溯到B;再扩展B到达D;D可行,此时A、B、D是活结点,D成为新的扩展结点;扩展D,先到达E;Cr<w3,E导致一个不可行解,回溯到D;再次扩展D到达F;由于F是叶结点,即得到一个可行解x=(1,0,0),V=45;
F不可扩展,成为死结点,返回到D;D没有可扩展结点,成为死结点,返回到B;B没有可扩展结点,成为死结点,返回到A;
A再次成为扩展结点,扩展A到达G;Cr=30,V=0,活结点为A、G,G为当前扩展结点;扩展G,先到达H;Cr=Cr-w2=15,V=V+v2=25,此时活结点为A、G、H,H成为当前扩展结点;扩展H,先到达I;Cr=Cr-w3=0,V=V+v3=50;I是叶结点,且50>45,皆得到一个可行解x=(0,1,1),V=50;I不可扩展,成为死结点,返回到H;
再扩展H到达J;J是叶结点,且25<50,不是最优解;J不可扩展,成为死结点,返回到H;H没有可扩展结点,成为死结点,返回到G;
再扩展G到达L;Cr=30,V=0,活结点为A、G、L,L为当前扩展结点;扩展L,先到达M,M是叶结点,且25<50,不是最优解,又M不可扩展,返回到L;
再扩展L到达N,N是叶结点,且0<50,不是最优解,又N不可扩展,返回到L;L没有可扩展结点,成为死结点,返回到G;G没有可扩展结点,成为死结点,返回到A
A没有可扩展结点,成为死结点,算法结束,最优解X=(0,1,1),最优值V=50
4.回溯法的应用步骤
针对所给问题,定义问题的解空间;
确定易于搜索的解空间结构;
以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
常用剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。
5.关于复杂性:
回溯法的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。
如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。
子集树:从n个元素的集合找出满足某种性质的子集,相应的解空间树称为子集树。 例如0-1背包问题。遍历子集树需O(2n)计算时间;
排列树:确定n个元素满足某种性质的排列,相应的解空间树称为排列树。排列树通常有n!个叶结点。因此遍历排列树需要O(n!)计算时间。例如8后问题、旅行售货员问题的解空间是一棵排列树。
6.经典例题:
6.1 回溯法求解0-1背包问题
解题的基本指导思想:
按贪心法的思路,优先装入价值/重量比大的物品。当剩余容量装不下最后考虑的物品时,再用回溯法修改先前的装入方案,直到得到全局最优解为止。
搜索解空间树策略:
只要其左儿子结点是一个可行结点,搜索就进入其左子树。—约束函数
当右子树有可能包含最优解时才进入右子树搜索,否则将右子树剪去。—限界函数
r:当前剩余物品价值总和;
cv:当前获得价值;
bestp:当前最优价值。
当cv+r<=bestp时,可剪去右子树。
将剩余物品依其单位重量价值排序,然后依次装入物品,直至装不下时,再装入该物品的一部分而装满背包。由此得到的价值是右子树中解的上界。
<span style="color:#000000;">package Algorithm;
public class Bag01Trace {
static int N=5;/*物品个数*/
static int cv=0;/*当前价值*/
static int cw=0;/*当前重量*/
static int bestv=0;/*当前最优价值*/
static int bestx[] = new int[N+1];/*当前最优解*/
static int C=10;/*背包容量*/
static int v[]={0,6,3,6,5,4};/*物品价值、重量。0元素不用*/
static int w[]={0,2,2,4,6,5};
static int x[]=new int[N+1]; //x[i]表示物品i当前是否加入背包
public static void main(String[] args) {
Backtrack(1);
putout();
}
private static void Backtrack(int i) {
int j;
if (i > N) /* 到叶结点 */
{
for (j = 1; j <= N; j++)
bestx[j] = x[j];
bestv = cv;
} else {
if (cw + w[i] <= C) /* 搜索左子树 */
{
x[i] = 1;
cw += w[i];
cv += v[i];
Backtrack(i + 1);
cw -= w[i];
cv -= v[i];
}
if (Bound(i + 1) > bestv)/* 搜索右子树 */
{
x[i] = 0;
Backtrack(i + 1);
}
}
}
private static int Bound(int i) {
int left = C - cw; // 剩余容量
int b = cv; // cv当前价值
while (i <= N&&w[i] <= left) {
b += v[i];
left -= w[i];
i++;
}
/* 装满背包 */
if (i <= N)
b += left * v[i] / w[i];
return b;
}
private static void putout() {
System.out.print("放入背包的物品是:");
cw = 0;
for (int i = 1; i <= N; i++)
if (bestx[i] == 1) {
System.out.print(i + " ");
cw += w[i];
}
System.out.println();
System.out.print("放入背包物品总重为:" + cw + " ");
System.out.print("最大价值和为:" + bestv);
}
}</span>
结果:
放入背包的物品是:1 2 3
放入背包物品总重为:8 最大价值和为:15
6.2 8皇后问题:http://blog.csdn.net/guoxiaowei400/article/details/40356407