基本思想
定义:以深度优先方式系统(全面)搜索问题的算法。
解空间表示:目的在于直观地看到所有解(包含最优解),把问题的所有可能解通过树或者图的方式列出,这样容易理解,且方便针对性的确定搜索方法。
回溯:以树为例,从一个结点出发深度遍历(根左右),每到一个结点就判断是否符合题目要求,若符合就继续,当遍历到叶子节点时就对比并记录下当前最优结果;若不符合,就直接返回上一结点遍历另一分支。
剪枝函数:用来判断是否继续沿当前结点遍历,避免无效搜索。通常有两种策略:(1)约束函数,剪去不满足约束的子树;(2)限界函数,剪去得不到最优解的子树。
算法步骤
- 针对所给问题,定义问题的解空间;
- 确定易于搜索的解空间结构;
- 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
两种回溯
(1)递归回溯
void backtrack(int t){
if(t>n) output(x);
else
for(int i=f(n,t);i<=g(n,t);i++){
x[t]=h(i);
if(constraint(t) && bound(t))
backtrack(t+1);
}
}
t:遍历的当前结点所在树深度,作为输入值,指定了从哪里开始遍历,一般传入1,搜索整个树;
n:空间树的最大深度;t>n表示已经搜到叶子结点,记录或输出该条搜索路径得到的解;
f(n,t)和g(n,t):分别代表当前结点的子树(没搜索过)的起始编号和终止编号;
h(i):表示在当前第i个子树,所选到的值;
x[t]:只记录当前在该层(t)的取值,所以可知当得到最优解,x中也只有最优解的路径;
constraint和bound:分别用来控制剪枝,决定是否继续延续该层该点往下遍历。
(2)迭代回溯
void iterativeBacktrack(){
int t=1;
while(t>0){
if(f(n,t)<=g(n,t))
for(int i=f(n,t);i<=g(n,t);i++){
x[t]=h(i);
if(constraint(t) && bound(t)){
if(solution(t)) output(x);
else t++;
}
}
else t--;
}
}
solution(t):用来判断是否到达叶子结点;
注意:搜索中我们要的是路径对应的值,不是结点,结点只用来连接。该值对应问题可选值。可以由下面的例子理解。
两种典型空间树
(1)子集树:所给问题的解是从n个元素的集合中,选出满足某种条件的子集。例:n个物品的0-1背包问题,有个叶结点,结点总数个,n=3时,子集树如图,路径值表示是否选择该物品。
回溯法搜索子集树(二叉树)的算法:
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);
}
}
(2)排列树:所给问题的解包含所有的n个元素,只是要满足某种条件的排列。例:包含n个城市的旅行售货员问题,有个叶结点,n=4时,排列树如图,路径值表示是否走该城市。
回溯法搜索排列树的算法:
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]);
}
}
类似于数组全排列问题。
参考文献
[1] 算法设计与分析(第2版) 王晓东