一、基本概念
回溯法是一种选优搜索法(试探法)。
基本思想:将问题P的状态空间E表示成一棵高为n的带全有序树T,把求解问题简化为搜索树T。搜索过程采用深度优先搜索。搜索到某一结点时判断该结点是否包含原问题的解,如果包含则继续往下搜索,如果不包含则向祖先回溯。
通俗来说,就是利用一个树结构来表示解空间,然后从树的根开始深度优先遍历该树,到不满足要求的叶子结点时向上回溯继续遍历。
几个结点:
扩展结点:一个正在产生子结点的结点称为扩展结点
活结点:一个自身已生成但未全部生成子结点的结点
死结点:一个所有子结点已全部生成的结点
二、基本步骤
1、分析问题,定义问题解空间。
2、根据解空间,确定解空间结构,得搜索树。
3、从根节点开始深度优先搜索解空间(利用剪枝避免无效搜索)。
4、递归搜索,直到找到所要求的的解。
三、两类常见的解空间树
1、子集树
当问题是:从n个元素的集合S中找出满足某种性质的子集时,用子集树。
子集树必然是一个二叉树。常见问题:0/1揹包问题、装载问题。
//遍历子集树伪代码
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);
}
}
}
遍历子集树时间复杂度:O(2^n)
2、排列树
当问题是:确定n个元素满足某种排列时,用排列数。常见问题:TSP旅行商问题,N皇后问题。
//遍历排列树伪代码
void backtrack(int t){
if(t>n){
output(x);
}else{
for(int i=0;i<=1;i++){
swap(x[t],x[i]);
if(legal(t)){
backtrack(t+1);
}
swap(x[t],x[i]);
}
}
}
遍历排列树时间复杂度:O(n!)
通俗地讲,结合Java集合的概念,选择哪种树其实就是看最后所得结果是放入一个List(有序)里,还是放入一个Set(无序)里。
四、剪枝函数
剪枝函数能极大提高搜索效率,遍历解空间树时,对于不满足条件的分支进行剪枝,因为这些分支一定不会在最后所求解中。
常见剪枝函数:
约束函数(对解加入约束条件)、限界函数(对解进行上界或下界的限定)
满足约束函数的解才是可行解。
五、求解模式
设问题的解是一个n维向量(a1,a2。。an),约束条件是ai之间满足某种条件,记为func(ai)
非递归回溯模式
int a[n],i;
//初始化数组a[];
i=1;
//有路可走,并且未达目标
while(i>0){
if(i>n){
//搜索到一个解,输出;
} else {
//a[i]第一个可能得值
while(a[i]在不满足约束条件且在搜索空间内){
a[i]下一个可能的值
}
}
}
递归回溯模式
一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度。
int a[n];
func(int i){
if(i>n){
//输出结果;
}else{
for(j=下界; j<=上界; j=j+1){ //枚举i所有可能得路径
if(func(j)){
a[i]=j;
}
}
}
}
常见案例问题
1、0/1揹包问题
2、TSP旅行商问题
3、最优装载问题
4、N-皇后问题
具体问题可百度详细内容。