回溯法の定义
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
以上来自百度百科。
回溯法の解空间
就是要找出这个问题所有的解,在这些解里面筛选出最优解。
举个例子,0-1背包的问题,假设我们有A,B,C三个物品,我们要判断选择装进哪些物品。从我们的背包里没有任何东西开始,我们按顺序一个个物品来判断是不是把它装进去。那么我们就可能出现两种想法:
想法一
对每一个物品判断是装还是不装,然后画出一个不规范的的图:
在这个图中,连线旁边的数字:1表示装入背包中,0表示不装入背包。第一层中,我们判断是否放入A,放入就是1那条岔路,反之是0那条岔路,所以很直观地,我们就可以得到所有可能出现的8个情况:
{(1,1,1),(1,1,0),(1,0,1),(1,0,0),(0,1,1),(0,1,0),(0,0,1),(0,0,0)}
想法二
我们假设自己一开始要从三个里面选一个来装入,所以就有了:
(但是01背包问题并不适合用这种方法来解,所以这里就是与上面的树做一个对比)
如上图则表示,我们从A,B,C三个中先选择一个装入,如果第一轮选择了A,那么剩下就还有选择B或者C
而想法一的想法,其实就能形成一颗子集树,它在每次选择的时候,是在一个有限选择集合S里面进行选择,01背包中,这个S就是{0,1},而想法二能形成一颗排列树,它在每次进行选择的时候是在给出的选项S中作选择,这里是{A,B,C}。
所以回溯法的写法,也有两种,子集树的写法和排列树的写法,做题的时候要自己选择更加合适的方法来解决。
回溯法の公式
每种算法都可以选择递归和迭代的方式来写,显然这里用迭代方式来写是很麻烦的,可以说也没有人选择这样的方法,所以我们都采用递归来写。
子集树写法
我们的目的就是要遍历子集树上的每个解法,为了判断每一个解对不对,所以我们应该是深度遍历,所以我们需要:t(树的深度),数组x(每一层的结果选择)。先写出中心的遍历函数:
void backtrack(int t){
if(t>=n){
output(); //此时已到达叶子结点,用于输出一个结果
return;
}
//对分叉点遍历0,1两种情况
for(int i=0;i<=1;i++){
x[t]=i; //给第t层代表的选择赋可能选择的值
if(ok(t))
//ok是判断目前的一个解有可行的机会
backtrack(t+1);
}
}
此时我们应该注意,x数组的初始值应该都为0。
排列树写法
也和子集树一样要遍历子集树上的每个解法,我们同样需要:t(树的深度),数组x(每一层的结果选择)。写出中心的遍历函数:
void backtrack(int t){
if(t>=n){
output(); //此时已到达叶子结点,用于输出一个结果
return;
}
//从第t个单元开始进行交换排列
for(int i=t;i<n;i++){
swap(x[t],x[i]); //交换两个排列顺序,得到了一个解
if(ok(t))
//ok是判断目前的一个解有可行的机会
backtrack(t+1);
swap(x[t],x[i]); //恢复原样,给后面的好继续交换判断
}
}
此时我们应该注意,因为排列树其实就是写出所有可能的排列,所以x数组里面每个单元的内容都应该是不一样的,我们在初始化的时候就应该依次赋予可能的解。
总结比较,在子集树的公式中,for循环中的i是可能的取值,排列树的公式中,for循环中的i是所有可能取值的存放位置。
举个栗子
n皇后问题,要求在一个n行n格的棋盘中摆放n个皇后,皇后不能在同一行,同一列或者同一斜线,我们要找出有多少个解法。这个问题,我们在选择x数组上,一个一维数组就足够用,下标表示在第几行,对应的值表示在第几列。
子集树方法:
假如n=4,初始化x数组中每个取值为0,那么我们的for循环中i可能有0,1,2,3四种选择,然后就是填写ok()函数,根据约束的条件可知,ok()函数里应该填写判断
皇后不能在同一行,同一列或者同一斜线
排列树方法:
假如n=4,初始化x数组中每个取值为0,1,2,3,ok函数也和子集树一样,我们的for循环中i也可能有0,1,2,3四种选择,因为是采用了交换,不出现两个重复的值,所以和子集树相比,走过的路要少一些。
效率の提高
为了提高算法的效率,我们可以从ok()函数来入手:
- 计算简单
- 剪枝多(避免走行不通的路)
但是这两个条件是互相矛盾的。
最后...
这是一个挣扎的小渣渣写的文章,所以可能会有错误,各位大佬多指教QAQ
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。