回溯法概述
——通用的解题方法、优化的穷举法。
一、
针对问题:需要找出它的解集,或者要求回答什么解时满足条件的最佳解时。
基本做法:搜索,或者时一种组织的井井有条的,能避免不必要搜索的穷举式搜索法(剪枝)。
主要利用:深度游先搜索策略
(1)递归回溯
(2)迭代回溯
(3)子集树算法框架
(4)排列树算法框架
二、直观印象:
(1)“通用的解题方法”,可以搜索所有解或最优解,系统的又带跳跃性的搜索算法。
(2)深度优先搜索解空间树。先判断结点是否包含问题的解。不包含跳过这个子树,包含进入子树深度优先搜索。
(3)求所有解、最优解:回溯到根,根节点的所有子树已经被搜索。求任意解:只要搜到一个解就可以。
任务描述:
(1)明确定义问题的解空间,每个解可以用解向量表示。
(2)并非解空间所有元素都是问题的解。
- 存在性问题:(可行解)求满足条件的一个或全部元组,不存在返回No
- 优化问题:(最优解)求满足条件,使目标函数最大的元组。
剪枝函数——减少问题求解所需时即生成的状态结点数。
①约束函数:提高效率,避免无谓搜索。
②限界函数:(最优化问题)剪去不可能包含最优结点的子树。
问题的解空间:
- 解向量:n元形式(x1,x2,x3……xn)
- 显约束:对xi取值限定(背包问题每件物品xi=0/1)
- 隐约束:为满足解,对不同分量施加的约束(wi小于背包容量)
- 解空间:满足显式约束的所有向量。
三、生成问题的基本方法:
1.
- 三个概念:
- 扩展结点:一个正在产生儿子的结点
- 活结点:一个自身已经生成,儿子还没有全部生成
- 死结点:一个所有儿子已经产生的结点。
2.
1.深度优先的问题状态生成法:
①扩展节点R,一旦产生一个儿子C,C就是新的扩展节点。R变为活结点。
②穷尽对子树C的搜索后,C变为死结点,R变为扩展节点。R继续生成下一个儿子。
2.宽度优先的问题状态生成法:
在一个扩展结点变为死结点之前,一直是扩展节点。
3.回溯:
限界函数处死一些不满足条件的活结点。
3. 解空间树:
1.子集树:解不定长
2.排列树:解定长
四、基本思想和步骤、代码框架:
有限离散问题总可以用穷举法求得问题的全部解。
求得了满足约束条件D的部分解(x1,x2,……xi) ,
①若存在xi+1使得(x1,x2,……xi,xi+1)满足D,继续添加xi+2;
②所有的xi+1均不满足,去掉xi,回到(x1,x2,……xi-1)继续进行。
解题步骤:
①针对所给问题,定义问题的解空间;
②确定易于搜索的解空间结构;
③以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
复杂性:
h(n)是根节点到叶子结点的最长距离。
只要一个解: O(h(n))
所有解:O(2^h(n))或O(h(n)!)
代码框架:
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))//约束条件&&限界条件
backrtrack(t+1);
}
}
2.迭代回溯:
/*f(n,t) g(n,t)表示在当前扩展结点处,未搜索过的结点的起始编号和终止编号*/
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--;//t变成了死结点,回溯
}
}
3.子集树O(2^n)
void backtrack(int t){
if(t>n) output(x);
else
for(int i=0;i<=1;i++){//只有两个儿子
x[t]=i;
if(legal(t)) backtrack(t+1);
}
}
4.排列树O(n!)
void backtrack(int t){
if(t>n) output(x);
else
for(int i=t;i<=n;i++){//每个结点i都可以放在t这个位置
swap(x[t],x[i]);
if(legal(t)) backtrack(t+1)
swap(x[t],x[i]);
}
}