回溯算法理论基础
回溯本质上就是递归,是递归的副产品。回溯函数也就指递归函数。 但回溯本质上也是穷举,某些时候可以加一些剪枝,但本质上仍然是一种暴力搜索。
回溯适用的问题
通常是一些比较困难,除了搜索所有解空间没别的办法的问题。
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
理解回溯
所有使用回溯法解决的问题都可以抽象为树结构。 回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
回溯三部曲模版
- 回溯函数的参数和返回值:返回值通常为空
- 回溯函数的结束条件 满足结束条件,就保存下结果,返回
- 回溯搜索的遍历过程
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
LeetCode 77 组合
思路
考虑一条选择路径,当已经选择m时,后面的数只能在m+1到n之间选择,否则就会对同一种组合重复访问(组合无序)
考虑一条未完成路径,其缺少的个数大于m+1~n之间的个数,已经无法满足这样的组合,可以直接剪枝返回
考虑回溯三要素:
- 回溯函数的参数和返回值 参数:要选的组合长度k,最大限制n 返回值:空 全局变量:结果集合result,当前选择路径path
- 回溯函数的结束条件 path长度为k时,把path副本记录在result中,返回 如果path最后一个数与n之间的个数小于仍需要选择的数值个数,返回
- 回溯搜索的遍历过程 假设m是path最后一个数,循环选择m+1~n之间的数 1. 加入path 2. 递归调用回溯函数 3. 还原path
解法
class Solution {
List<List<Integer>> result;
List<Integer> path;
public List<List<Integer>> combine(int n, int k) {
result = new ArrayList<>();
path = new ArrayList<>();
backTracking(n, k);
return result;
}
public void backTracking(int n, int k) {
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
int depth = path.size();
// 剪枝
if (depth != 0 && k - depth > n - path.get(depth - 1)) {
return ;
}
// 横向遍历
int i;
if (depth == 0) {
i = 1;
}
else {
i = path.get(depth - 1) + 1;
}
for (; i < n+1; i++) {
path.add(i);
backTracking(n, k);
path.remove(path.size() - 1);
}
}
}
LeetCode 216 组合总和III
思路
使用k个不同的1~9之间的数,组合出和为n的组合
考虑剪枝:
- 和上题类似,由于组合无序,选择数字时只能向后选。如果可供选择的数量小于仍需要的数量,就无法在子树里找到满足要求的组合
- 本题的条件有求和,如果可供选择的数字全部大于现在的组合中缺的数值,也可以返回。
- 同样如果和sum已经大于等于目标和n,且数量不够,返回
考虑回溯三要素:
- 回溯函数的参数和返回值 参数:总和n,个数k 返回值:空 全局变量:结果集合result,选择路径path,选择路径的和sum
- 回溯函数的结束条件 path长度等于k且sum等于n,记录到result,返回 path满足剪枝条件,返回
- 回溯搜索的遍历过程 对FROM path最后一个数+1 TO 9进行遍历 1. 把数加入path,更新sum 2. 递归调用回溯函数 3. 还原path和sum
解法
class Solution {
List<List<Integer>> result;
List<Integer> path;
int sum;
public List<List<Integer>> combinationSum3(int k, int n) {
result = new ArrayList<>();
path = new ArrayList<>();
sum = 0;
backTracking(k, n);
return result;
}
public void backTracking(int k, int n) {
int depth = path.size();
if (depth == k) {
if (sum == n) {
result.add(new ArrayList<>(path));
}
return ;
}
if (sum >= n) {
return ;
}
if (depth != 0 && (k-depth) > (9-path.get(depth-1))) {
return ;
}
if (depth != 0 && path.get(depth-1)+1 > n-sum) {
return ;
}
int i;
if (depth == 0) {
i = 1;
}
else {
i = path.get(depth-1)+1;
}
for (; i < 10; i++) {
path.add(i);
sum += i;
backTracking(k, n);
sum -= i;
path.remove(path.size()-1);
}
}
}
LeetCode 17 电话号码的字母组合
思路
先构造出一个int映射到字符list的map,方便查阅
考虑回溯三要素:
- 回溯函数的参数和返回值 参数:数字字符串digits 返回值:空 全局变量:map,结果集合result,选择路径path
- 回溯函数的结束条件 path长度等于digits,path的副本加入result
- 回溯搜索的遍历过程 对下一个数字,循环其可以对应的字母 1. 把字母加入path 2. 递归调用回溯函数 3. 把path还原
解法
class Solution {
Map<Character, List<Character>> map;
StringBuilder path;
List<String> result;
public List<String> letterCombinations(String digits) {
map = new HashMap<>();
map.put('2', Arrays.asList('a', 'b', 'c'));
map.put('3', Arrays.asList('d', 'e', 'f'));
map.put('4', Arrays.asList('g', 'h', 'i'));
map.put('5', Arrays.asList('j', 'k', 'l'));
map.put('6', Arrays.asList('m', 'n', 'o'));
map.put('7', Arrays.asList('p', 'q', 'r', 's'));
map.put('8', Arrays.asList('t', 'u', 'v'));
map.put('9', Arrays.asList('w', 'x', 'y', 'z'));
result = new ArrayList<>();
path = new StringBuilder();
if (digits.length() == 0) {
return result;
}
backTracking(digits);
return result;
}
public void backTracking(String digits) {
if (digits.length() == path.length()) {
result.add(path.toString());
return ;
}
char digit = digits.charAt(path.length());
for (char ch : map.get(digit)) {
path.append(ch);
backTracking(digits);
path.deleteCharAt(path.length()-1);
}
}
}
今日收获总结
今日学习时长2小时,回溯思想和递归类似,但回溯需要对某些状态的还原多加考虑