回溯法
回溯法往往用来找出一个问题的所有可能解(而不是简单的回答出有多少个解,如果是问解的个数,可能有更好的办法,也就是动态规划)。
回溯法是DFS的一种,但又和DFS不一样,DFS往往求解连通面积,可达性的问题,而回溯法往往求解满足某一条件的所有组合,比如二叉树的路径和,排列组合等经典问题。
回溯法在代码层面,最明显的特征就是
- 终止条件的判断
- 如果当前组合满足条件,加入结果集
- 开始遍历元素
- 把某一元素加入组合
- 递归调用
- 再把该元素剔除组合
以 257. 二叉树所有路径 为例,进行简单的解释:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
if(root == null){
return new ArrayList<String>();
}
List<String> res = new ArrayList<String>();
backtracking(root,new ArrayList<Integer>(),res);
return res;
}
private void backtracking(TreeNode node, List<Integer> list,List<String> res){
if (node == null)
return;
//4,把某一元素加入组合
list.add(node.val);
//1 ,终止条件的判断
if(node.left == null && node.right == null){
//2,如果当前组合满足条件,加入结果集
res.add(buildPath(list));
}else{
//3 开始遍历元素 这个例子有些特殊,可以看作是遍历左右子树
//5 递归调用backtracking
backtracking(node.left, list, res);
backtracking(node.right, list, res);
}
//6,再把该元素剔除组合
list.remove(list.size()-1);
}
private String buildPath(List<Integer> list){
int size = list.size();
StringBuilder sb = new StringBuilder();
for(int i =0;i<size;i++){
sb.append(list.get(i));
if(i != size-1)
sb.append("->");
}
return sb.toString();
}
}
其实这些是最基本的,如果想要达到回溯法的进阶要求,还得往下看。
其实回溯法最经典的应用当属排列组合,这是两个问题,往往有以下几种形式:
排列+无重复元素
排列+有重复元素
组合+无重复元素
组合+有重复元素
区别在哪里呢?
排列与组合的区别
排列要考虑元素的顺序,顺序不一样,是不一样的组合,需要引入boolean数组进行标识,是否已经在当前的组合里了。
组合不需要考虑顺序。
有重复元素和无重复元素
如果有重复元素,往往需要排序+利用boolean数组去重,因此经常会用到这一段代码:
for(int i = 0;i<nums.length;i++){
if(visited[i])
continue;
//相对于无重复元素 新增逻辑
if(i>0&&nums[i]==nums[i-1]&&!visited[i-1])
continue;
visited[i] = true;
list.add(nums[i]);
backtracking(nums,res,list,visited);
list.remove(list.size()-1);
visited[i] = false;
}
无重复元素,也要注意,元素个数是否可以无限制。
问题杂记:
- 初始化
List<List<Integer>> res = new ArrayList<>();
- 加入结果集时要当心
if(list.size() == nums.length){
//这里有一个坑,不能直接res.add(list);
//因为list后面会变化,这样做相当于,res里所有元素都指向list的引用
//会一模一样
res.add(new ArrayList<>(list));
return;
}
- 剪枝
也就是遍历的时候不是从0开始,这个要看情况使用,往往用于组合+无重复元素
//不是从0开始
for(int i = startIndex;i<candidates.length;i++){
list.add(candidates[i]);
backtracking(candidates,leftValue-candidates[i],i,res,list);
list.remove(list.size()-1);
}
- boolean数组
什么情况下需要它呢?
1 有重复元素时(主要还是用来去重)
2 无重复元素+排列(标记该元素是否使用过)