目录
1.回溯算法简单介绍
2.回溯算法框架:
我们用一道题来详细讲解回溯算法的过程
3.全排列问题
1.回溯算法简单介绍
解决一个回溯问题,其实就是一个决策树的遍历过程,我们只需要思考三个问题:
1.路径:就是已经做出的选择
2.选择列表:就是当前我们可以做的选择
3.结束条件:就是到达决策树底层,无法在作出选择的条件
2.回溯算法框架:
核心就是for循环里面的递归,在递归调用前做出选择,在递归调用后撤销选择
backtrack(路径,选择列表){
//结束条件
结果加入结果集
for 选择 in 列表:
//做选择,并将该选择从列表中移除
路径.add(选择)
backtrack(路径,选择列表)
//撤销选择
路径.remove(选择)
}
我们用一道题来详细讲解回溯算法的过程
3.全排列问题
为了简单清晰的讲解,我们这次的全排列不包含重复数字
假设给我们三个数【1,2,3】,我们可以轻而易举的画出回溯树:
只要从根节点遍历这棵树,记录路径上的数字,走到叶子节点就得到了一个排列,遍历完整棵树就得到了所有的全排列,我们把它称之为决策树
我们定义的backstrack函数其实就像一个指针,在这棵树上遍历,同时维护每个节点的属性,每当走到树的底层,其路径就是一个全排列
那么如何遍历一棵树呢?
多叉树的遍历框架:
void traverse(TreeNode root){
for(TreeNode child:root.children) {
//前序遍历需要的操作
traverse(child);
//后序遍历需要的操作
}
}
前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历的代码在离开某个节点的之后的那个时间点执行
所以我们只要在递归调用前做出选择,在递归调用后撤销选择,就能正确维护每个结点的选择列表和路径
下面我们来看全排列的代码:
public class Solution {
List<List<Integer>> res = new LinkedList<>();
public static void main(String[] args) {
int[] nums = {1,2,3};
Solution s = new Solution();
System.out.println(s.permute(nums));
}
public List<List<Integer>> permute(int[] nums) {
LinkedList<Integer> list = new LinkedList<>();
permuteHelper(nums,list);
return res;
}
private void permuteHelper(int[] nums, LinkedList<Integer> list) {
//边界条件
if(list.size() == nums.length) {
res.add(new LinkedList<>(list));
return;
}
//做出选择
for (int num: nums) {
//在递归之前做出选择
//判断是否已选择过
if(list.contains(num)) {
continue;
}
list.add(num);
permuteHelper(nums,list);
//在递归之后撤销选择
list.removeLast();
}
}
}
至此,我们就通过全排列问题详解了回溯算法的底层原理,当然这么做不是最好的思路,但是必须说明说我是,不管怎么优化,都符合回溯框架,而且时间复杂度不可能低于O(N!),因为穷举整颗决策树是不可避免的,这也是回溯算法的一个特点,不想动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高
从某种程度上说,动态规划的暴力求解阶段就是回溯算法,只是有的问题可以通过巧妙的定义,构造出最优子结构,找到重叠子问题,用dp数组或者备忘录优化,将递归树大幅剪枝,这就是动态规划