之前已经学习过回溯法的一些问题,从这篇文章开始,继续深入学习一下回溯法以及其他经典问题。
回溯法有通用的解题法之称。用它可以系统的搜索一个问题的所有解或任一解,回溯法是一个既带有系统性又带有跳跃性的搜索算法。
它的问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先策略搜索。回溯法求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。回溯法求问题的一个解时,只要搜索到问题的一个解就可结束。
这种以深度优先方式搜索问题解的算法称为回溯法,它适用于解组合数较大的问题。
回溯法的算法框架:
1、问题的解空间
用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少应包含问题的一个(最优解)。
2、回溯法的基本思想
确定了解空间的组织结构后,回溯法从开始结点出发,以深度优先方式搜索整个解空间。这个开始结点称为活结点,同时也称为当期那的扩展结点,如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就称为死结点。此时,应往回移动(回溯)至最近的一个或活结点处,并使这个活结点称为当前的扩展结点。回溯法以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止。
3、递归回溯
回溯法对解空间作深度优先搜索,因此在一般情况下可用递归函数来实现回溯法如下:
void Backtrack(int t) { if(t > n) //t>n时已搜索到一个叶结点,output(x)得到的可行解x进行记录或输出处理 Output(x); else //当前拓展结点是解空间树的内部结点 { for(int i = f(n,t); i <= g(n, t); i++) //函数f和g分别表示当前扩展结点处未搜索子树的起止编号 { x[t] = h(i); //h(i)表示在当前扩展结点处x[t]的第i个可选值 if(Constraint(t) && Bound(t)) Backtrack(t+1); } //循环结束时,已搜索遍当前扩展结点的所有未搜索子树 } }
其中,形式参数t表示递归深度,即当前扩展结点在解空间树中的深度。n用来控制递归深度,当t>n时,算法已搜索到叶结点,此时,由Output(x)记录或输出得到的可行解x。算法BackTrack的for循环中f(n,t)和g(n,t)分别表示在当前扩展结点处未搜索过的子树的起始编号和终止编号。h(i)表示在当前扩展结点处x[t]的第i个可选值。Constraint(t)和Bound(t)表示在当前扩展结点处的约束函数和限界函数。Constraint(t)返回的值为true时,在当前扩展结点处x[1:t]的取值满足问题的约束条件,否则不满足问题的约束条件,可剪去相应的子树。
Bound(t)返回的值为true时,在当前扩展结点处x[1:t]的取值未使目标函数越界,还需由Backtrack(t+1)对其相应的子树做进一步搜索。
否则,当前扩展结点处x[1:t]的取值使目标函数越界,可剪去相应的子树。执行了算法的for循环后,已搜索遍当前扩展结点的所有未搜索过的子树。Backtrack(t)执行完毕,返回t-1层继续执行,对还没有测试过的x[t-1]的值继续搜索。当t=1时,若已测试完x[1]的所有可选值,外层调用就全部结束。显然,这一搜索过程按深度优先方式进行,调用一次Backtrack(1)即可完成整个回溯搜索过程。
4、迭代回溯
采用树的非递归深度优先遍历算法,也可将回溯法表示为一个非递归的迭代过程如下:
void IterativeBacktrack() { int t; t = 1; //当前扩展结点在解空间树中的深度,在这一层确定解向量的第t个分量x[t]的取值 while(t > 0) { if(f(n,t) <= g(n,t)) //f和g分别表示在当前扩展结点处未搜索子树的起止编号 { for(int i = f(n,t); i <= g(n,t); i++) { x[t] = h(i); //h(i)表示在当前扩展结点处x[t]的第i个可选值 if(Constraint(t) && Bound(t)) { if(Solution(t)) //solution(t)判断当前扩展结点处是否已得到问题的一个可行解 Output(x); else t++; //solution(t)为假,则仅得到一个部分解,需继续纵深搜索 } } } else t--; //如果f(n,t)>g(n,t),已搜索遍当前扩展结点的所有未搜索子树, } //返回t-1层继续执行,对未测试过的x[t-1]的值继续搜索 }
上述迭代回溯算法中,用Solution(t)判断在当前扩展结点处是否已得到问题的可行解。它返回的值为true时,在当前扩展结点处x[1:t]是问题的可行解。此时,由Output(x)记录或输出得到的可行解。它返回的值为false时,在当前扩展结点处x[1:t]只是问题的部分解,还需向纵深方向继续搜索。
算法中f(n,t)和g(n,t)分别表示在当前扩展结点处未搜索过的子树的起始编号和终止编号。h(i)表示在当前扩展结点处x[t]的第i个可选值。Constraint(t)和Bound(t)是当前扩展结点处的约束函数和限界函数。Constraint(t)的返回的值为true时,在当前扩展结点处x[1:t]的取值满足问题的约束条件,否则不满足问题的约束条件,可剪去相应的子树。Bound(t)返回的值为true时,在当前扩展结点处x[1:t]的取值未使目标函数越界,还需对其相应的子树做进一步搜索。否则,当前扩展结点处x[1:t]的取值已使目标函数越界,可剪去相应的子树。算法的while循环结束后,完成整个回溯搜索过程。
5、字集树与排列树
当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间数称为子集树。这类子集树通常有个叶结点,其结点总个数为.遍历子集树的任何算法均需的计算时间。
void Backtrack(int t) { if(t > n) Output(x); else { for(int i = 0; i <= 1; i++) { x[t] = i; if(Constraint(t) && Bound(t)) Backtrack(t+1); } } }
当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶结点。因此遍历排列数需要的计算时间。
void Backtrack(int t) { if(t > n) Output(x); else { for(int i = t; i <= n; i++) { swap(x[t], x[i]); if(Constraint(t) && Bound(t)) Backtrack(t+1); swap(x[t], x[i]); } } }
在调用Backtrack(1)执行回溯搜索之前,先将变量数组x初始化为单位排列(1,2,....,n)