参考链接:
https://segmentfault.com/a/1190000018319044?utm_source=tag-newest
https://www.cnblogs.com/xiaobaidashu/p/10724789.html
https://zhuanlan.zhihu.com/p/93530380
1.
回溯法是一种系统搜索问题解空间的方法。
根据定义来看,要实现回溯,需要两点1搜索,2解空间
先看什么是解空间。
就是形如数组的一个向量[a1,a2,…,an]。这个向量的每个元素都是问题的部分解,只有当这个数组的每一个元素都填满(得到全部解)的时候,才表明这个问题得到了解答。
再看搜索。
最简单的就是for循环,上面的向量有n个维度,因此就是n个for循环。
形如:
for(求a1位置上的解)
for(求a2位置上的解)
for(求a3位置上的解)
......
......
for(求an位置上的解)
但是如果n是100?n是100000?那么如何回溯?
当然也可以写n个for循环,但是这样的程序会惨不忍睹。。。而且似乎10000个(不过往往回溯的时间复杂度太大,一般n不会这么大)for循环也很难写出来。。。
因此我们需要一种全新的书写回溯的方法。形如:
void backtrack(int i,int n,other parameters)
{
if( i == n)
{
//get one answer
record answer;
return;
}
//下面的意思是求解空间第i个位置上的下一个解
for(next ans in position i of solution space)
{
backtrack(i+1,n,other parameters);
}
}
上面的i代表解空间的第i个位置,往往从0开始,而n则代表解空间的大小。每一次的backtrack(i,n,other)调用,代表求解空间第i个位置上的解。而当i=n时,代表解空间上的所有位置的解都已经求出。
2.
回溯算法实际上一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回(也就是递归返回),尝试别的路径。
递归函数的参数的选择,要遵循四个原则
1、必须要有一个临时变量(可以就直接传递一个字面量或者常量进去)传递不完整的解,因为每一步选择后,暂时还没构成完整的解,这个时候这个选择的不完整解,也要想办法传递给递归函数。也就是,把每次递归的不同情况传递给递归调用的函数。
2、可以有一个全局变量,用来存储完整的每个解,一般是个集合容器(也不一定要有这样一个变量,因为每次符合结束条件,不完整解就是完整解了,直接打印即可)。
3、最重要的一点,一定要在参数设计中,可以得到结束条件。一个选择是可以传递一个量n,也许是数组的长度,也许是数量,等等。
4、要保证递归函数返回后,状态可以恢复到递归前,以此达到真正回溯。
3.
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
代码方面,回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
写 backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。