回溯算法
什么是回溯法?
回溯法指的是回溯搜索法,是一种搜索方式。比如在二叉树的搜索中,在递归的过程中实际上蕴涵了回溯,有递归就有回溯,回溯函数即为递归函数。
回溯法的性能
回溯并不算高效,其本质还是穷举所有可能,从中找出我们想要的答案,相当于暴力搜索。
回溯法的题目类型
- 组合问题:给定一个集合,找出满足某个条件的所有组合,不考虑元素顺序
- 切割问题:给定一个字符串,问有哪几种切割方式
- 子集问题:给定一个集合,列出所有子集
- 排列问题:和组合问题类似,但强调元素顺序
- 棋盘问题:N皇后,数独等
这些问题用普通的迭代for循环是没有办法解决的,需要用到回溯的穷举。
如何理解回溯?
回溯法的相关问题其实都可以转化为树形结构。回溯法的思路都是在集合中递归查找子集,集合的大小为树的宽度,递归的次数决定了树的深度。回溯也一定会有终止条件,因此这个树是高度有限的N叉树。
回溯法模板
回溯算法一般没有返回值,主要是用来在最后收集结果。参数可能在一开始没法明确确定下来,因此一般是先写逻辑再补需要的参数。和递归一样,首先要确认回溯终止条件并存放结果。回溯中的for循环是横向遍历,也就是遍历一个节点所有的孩子,每次循环中的递归函数就是纵向遍历。
public void backtracking(参数) {
if (终止条件) {
收集结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果;
}
}
LeetCode - 77. Combinations 组合
解题目标:给定两个整数n和k,列出所有长度为k的并且范围为在[1,n]的所有组合。
解题思路:假如没有回溯算法,这道题比较浅显的想法就是假如k=2,那么就嵌套两层for循环把所有的可能情况给打印出来,但是如果k是一个很大的值,那么显然这个思路是不可行的。回溯算法的目的就是用递归来简化for循环的操作。假如k=2的话,这里的回溯法就是两层回溯。注意组合的定义是不注重顺序的,e.g. {1, 2}和{2, 1}是重复的组合。
class Solution {
//全局变量,用于收集最终的结果以及当前的集合
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
//startIdx是每一次循环开始的的元素下标
public void backtracking(int n, int k, int startIdx) {
//终止条件,当收集满k个元素后加入结果集
if (path.size() == k) {
res.add(new ArrayList(path));
return;
}
//对于其中一层
for (int i = startIdx; i <= n; i++) {
path.add(i);
backtracking(n, k, i+1);
path.removeLast(); //回溯
}
}
}
LeetCode 216. Combination Sum III 组合总和III
解题目标:给定整数k和n,接触所有长度为k的由数字[1, 9]组成相加等于n的组合
解题思路:这题和上一题的思路类似,但是回溯的时候多了一个当前的sum,并且在终止条件要加上sum==n
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> comb = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k, n, 1, 0);
return res;
}
public void backtracking(int k, int n, int startIdx, int sum) {
if (comb.size() == k && sum == n) {
res.add(new ArrayList(comb));
return;
}
for (int i = startIdx; i <= 9; i++) {
comb.add(i);
backtracking(k, n, i + 1, sum + i);
comb.removeLast();
}
}
}
LeetCode - 17. 电话号码的字母组合
解题目标:给定一个数字字符串,找出对应的不同的数字相对应的字母组合
解题思路:这道题因为是涉及到数字映射为字母,这里可以使用二维数组来做映射,因为数字可以当成二维数组外层的下标来使用,这样就可以用数字直接找到数字所对应的字母集。但是当在搜索字母组合的时候,如果碰到多个数字还使用嵌套多个for循环的暴力解法依然会导致代码冗余,因此这题还是要用到回溯法。这里需要注意,回溯函数中这里不需要startIdx,因为我们这时候求的是两个集合中的组合,这个index应该是表示当前数字字符串中遍历到的index,和要去map中找字母的index不一样。map中对应的是数字字母对应的值,因此需要减去字母0来算出ASCII码转换成真正的index。
重点是下一层的递归中我们要传入的是下一位数字的index,因此这道题关键点在于搞清楚什么是当前数字字符串遍历到的index,以及什么是map中寻找字母要用到的index。
class Solution {
private List<String> res = new ArrayList<>();
StringBuilder comb = new StringBuilder();
private String[] letterMap = {
"", // 0
"", // 1
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"};
public List<String> letterCombinations(String digits) {
if (digits.length() == 0) return res;
backtracking(digits, 0);
return res;
}
public void backtracking(String digits, int digitIdx) {
//终止条件,当遍历到最后一个数字遍历完后才开始收集结果
if (digitIdx == digits.length()) {
res.add(new StringBuilder(comb).toString());
return;
}
//获取数字(字母)对应下标
int cur_digit = digits.charAt(digitIdx) - '0';
//获取数字所对应的字母集
String letters = letterMap[cur_digit];
for (int i = 0; i < letters.length(); i++) {
comb.append(letters.charAt(i));
//下一层递归应该对应下一个数字
backtracking(digits, digitIdx + 1);
comb.deleteCharAt(comb.length() - 1); //回溯
}
}
}