回溯:
有递归就会有回溯,递归和回溯相辅相成,回溯通常藏在递归函数的下面。
回溯函数其实就是递归函数,没有单独的回溯函数
效率:
回溯函数是一个纯暴力的搜索。
有的问题用for循环一点一点搜索都搜索不出来,一定要用回溯才能搜索出来
哪些问题for循环一点一点搜索都搜索不出来,一定要用回溯才能搜索出来?
组合问题。
e.g. 1234有多少种两两组合? 12 13 14.。。。
切割问题。
e.g. 给一个字符串,有几种切割方式?
子集问题。
e.g. 1234的子集有哪些?1 2 3 4 12 13 14。。。
排列问题。
e.g. 12的排列有12和21.
棋盘问题。
e.g. N皇后、解数独
怎么理解回溯法?
把回溯法抽象为图像,做题的时候要画图。
回溯法可以抽象为N叉树,树的宽度是要处理的集合的大小,深度是递归的深度(要通过递归处理)
回溯的模板
在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。
回溯算法中函数返回值一般为void。
void backtracking(参数){
//什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
if (终止条件) {
存放结果;
return;
}
//回溯函数遍历过程伪代码如下
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归,根据树,从上往下递归
回溯,撤销处理结果
}
}
//为什么要撤销处理结果?
//比如123排列组合问题
//12处理完以后,要把12撤销,组合13;13处理完以后,要把13撤销,组合23
回溯三部曲
递归函数参数返回值
确定终止条件
单层递归逻辑
回溯的题:
39. 组合总和 - 力扣(LeetCode)
class Solution {
//全局变量
List<Integer> path = new LinkedList();
List<List<Integer>> result = new LinkedList();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates, target, 0, 0);
return result;
}
//回溯
public void backtracking(int[] candidates, int target, int sum, int startindex){
//确定终止条件
if (sum > target){
return;
}
if (sum == target){
result.add(new LinkedList(path));//每次要复制一个path,不能在原path基础上操作
return;
}
//回溯+递归
for(int i=startindex; i < candidates.length && sum <= target; i++){
path.add(candidates[i]);
sum += candidates[i];
//单条路径递归
backtracking(candidates, target, sum, i);
//回溯,撤销上一步
sum -= candidates[i];
path.removeLast();
}
}
}
46. 全排列 - 力扣(LeetCode)
class Solution {
List<Integer> path = new LinkedList();
List<List<Integer>> result = new LinkedList();
public List<List<Integer>> permute(int[] nums) {
boolean[] used2 = new boolean[nums.length];
backtracking(nums, used2 );
return result;
}
public void backtracking(int[] nums, boolean[] used){
//确定终止条件
if (nums.length == path.size()){
result.add(new LinkedList(path));
return;}
//递归+回溯
for(int i = 0; i < nums.length; i++){
if (used[i] == true){
continue;
}
path.add(nums[i]);
used[i] = true;//标记已使用
backtracking(nums, used);
//回溯,撤销上一步
//回溯底层是栈,先入后出,先加元素,此时后移除元素
used[i] = false;
path.remove(path.size()-1);
}
}
}
78. 子集 - 力扣(LeetCode)
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
什么时候for可以从0开始呢?
求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合。
class Solution {
List<Integer> path = new LinkedList();
List<List<Integer>> result = new LinkedList();
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums, 0);
return result;
}
//无序,需要startindex
public void backTracking (int[] nums, int startindex){
//每种可能的结果都要
result.add(new LinkedList(path));
//终止条件
if (startindex >= nums.length){
return;
}
//回溯+递归
for(int i=startindex; i<nums.length; i++){
path.add(nums[i]);
backTracking(nums, i+1);//注意:这里是i+1,和组合问题不同,这里每次递归就是一个结果,直接调用下一次递归即可
//回溯
path.remove(path.size()-1);
}
}
}
17. 电话号码的字母组合 - 力扣(LeetCode)
class Solution {
List<String> result = new LinkedList();
StringBuilder path = new StringBuilder();
public List<String> letterCombinations(String digits) {
// 处理空输入情况
if (digits.equals("") || digits == null) return result;
String[] telephone = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
backtracking(digits, 0, telephone);
return result;
}
//1.确定返回值和参数
void backtracking (String digits, int index, String[] tele){
//2.确定终止条件
if (index == digits.length()){
result.add(path.toString());
return;
}
//3.单层递归逻辑
//str表示数字对应的字母,如2-abc
String str = tele[digits.charAt(index) - '0'];
for (int i = 0; i < str.length(); i++){
path.append(str.charAt(i));
backtracking(digits, index+1, tele);
path.deleteCharAt(path.length()-1);
}
}
}
22. 括号生成 - 力扣(LeetCode)
class Solution {
List<String> result = new LinkedList();
StringBuilder path = new StringBuilder();
public List<String> generateParenthesis(int n) {
//其实就是左括号和右括号的有效组合
//有序的:左括号一定在右括号前面(通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点)
//如果左括号数量小于 n,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。
backtracking(n, 0, 0);
return result;}
//回溯
//1.确定返回值和参数
//返回值:void
//参数:括号对数量,n;记录当前递归时左括号的数量:left;记录当前递归时右括号的数量:right
public void backtracking(int n, int left, int right){
//2.终止条件
if (path.length() == 2*n){
result.add(path.toString());
return;
}
//3.单层递归逻辑:如果左括号数量小于 n,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。
if (left < n){
path.append('(');
backtracking(n, left+1, right);
path.deleteCharAt(path.length()-1);
}
if (right < left){
path.append(')');
backtracking(n, left, right+1);
path.deleteCharAt(path.length()-1);
}
}
}