算法 ---- LeetCode回溯系列问题题解
Posted TheWhc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法 ---- LeetCode回溯系列问题题解相关的知识,希望对你有一定的参考价值。
回溯法
回溯
1. 什么是回溯?
回溯法也叫回溯搜索法,是一种搜索的方式
2. 回溯法解决的问题
- 组合问题: N个数里面按一定规则找出k个数的集合
- 切割问题: 一个字符串按一定规则有几种切割方式
- 子集问题: 一个N个数的集合里有多少符合条件的子集
- 排列问题: N个数按一定规则全排列,有几种排列方式
- 棋盘问题: N皇后,解数独等等
组合是不强调元素顺序,排列强调元素顺序
例如: {1,2}和{2,1}在组合上,是一个集合。
对排列来说, {1,2}和{2,1}就是两个集合了
3. 回溯法的模板
-
回溯函数模板返回值以及参数
返回值一般为void,参数一开始不能一次性确定,所以一般先写逻辑,然后需要什么参数,就填充什么参数
void backtrack(参数)
-
回溯函数终止条件
一般来说搜到叶子节点,即找到满足条件的一条答案,就把答案存放起来,并结束本层递归。
if (终止条件) { 存放结果; return; }
-
回溯搜索的遍历过程
回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成树的深度
for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtrack(路径,选择列表); // 递归 回溯,撤销处理结果 }
for循环就是遍历集合区间,可以理解一个节点有多少孩子,for循环就执行多少次。
(for循环横向遍历,backtrack纵向遍历)
综合,回溯算法模板如下:
void backtrack(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtrack(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
LeetCode题目列表
组合问题
77. 组合
/**
* 思路: 回溯法
* n相当于树的宽度, k相当于树的高度
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
* 函数一定要有n和k,还需要有一个参数为int类型的startIndex,用于记录本层递归中,集合从哪里开始遍历
*
* 2. 回溯函数终止条件
* path数组大小等于k的时候,保存起来,结束本层递归
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始
*/
/*List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
if(n <= 0 || k <= 0) {
return res;
}
List<Integer> path = new ArrayList<>();
backtrack(n, k, 1);
return res;
}
private void backtrack(int n, int k, int start) {
if(path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= n; i++) {
path.add(i);
backtrack(n, k, i+1);
path.remove(path.size()-1);
}
}*/
// 剪枝优化
// 遍历的范围是可以优化的,比如n=4,k=4时,第一层for循环开始,从2开始就没有意义了。第二层for循环,从3开始就没有意义了
// 因此每层for循环最多到达 n - (k - path.size()) + 1
// n - 还需要选择的元素大小 + 1
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
if(n <= 0 || k <= 0) {
return res;
}
List<Integer> path = new ArrayList<>();
backtrack(n, k, 1);
return res;
}
private void backtrack(int n, int k, int start) {
if(path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
backtrack(n, k, i+1);
path.remove(path.size()-1);
}
}
216 组合总和III
/**
* 思路: 回溯法
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
* 函数一定要有targetSum和k,还需要有一个参数为int类型的startIndex,用于记录本层递归中,集合从哪里开始遍历
* targetSum表示减去当前选择的元素后, 还需要多少目标和
*
* 2. 回溯函数终止条件
* path数组大小等于k并且targetSum == 0时,保存起来,结束本层递归
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始
*/
/*List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
if(k <= 0) {
return res;
}
backtrack(n, k, 1);
return res;
}
private void backtrack(int targetSum,int k, int startIndex) {
if(targetSum < 0) {
return;
}
if(targetSum == 0 && k == path.size()) {
res.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= 9; i++) {
path.add(i);
targetSum -= i;
backtrack(targetSum,k, i+1);
targetSum += i;
path.remove(path.size()-1);
}
}*/
// 剪枝优化
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
if(k <= 0) {
return res;
}
backtrack(n, k, 1);
return res;
}
private void backtrack(int targetSum,int k, int startIndex) {
// 剪枝优化
if(targetSum < 0) {
return;
}
if(targetSum == 0) {
if(k == path.size()) {
res.add(new ArrayList<>(path));
}
// 可能出现不满大小为k的路径, 虽然满足targetSum == 0,但是依然提前返回
return;
}
// 剪枝优化
// 范围优化: 9 - (k - path.size()) + 1
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
backtrack(targetSum - i,k, i+1);
path.remove(path.size()-1);
}
}
17. 电话号码的字母组合
/**
* 思路: 回溯法
* 1. 构建一个数组对应字母与数字之间的映射关系
*
* 2. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* 函数参数(String digits, int startIndex)
* digits: 每层选择列表
* startIndex: 记录本层递归中, 当前遍历到了哪一个数字
*
* 3. 回溯函数终止条件
* path大小等于digits大小, 保存到结果集, 结束本层递归
*
* 4. 递归函数每层for循环,从0开始遍历到每个数字映射的字符串的大小
*/
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
String[] letterMap = new String[]{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length() == 0) {
return res;
}
backtrack(digits, 0);
return res;
}
private void backtrack(String digits, int startIndex) {
if(path.length() == digits.length()) {
res.add(new String(path.toString()));
return;
}
// 比如digits = "23", startIndex = 0对应字符'2', 则第一层遍历从0开始遍历到第一个字符的'2'的字符串"abc"大小为3
String letters = letterMap[digits.charAt(startIndex) - '0'];
for (int i = 0; i < letters.length(); i++) {
// 选择元素
// "abc".charAt(0) = 'a'
char c = letters.charAt(i);
path.append(c);
// 递归进入下一层 startIndex + 1 对应字符'3'
backtrack(digits, startIndex + 1);
path.deleteCharAt(path.length()-1);
}
}
39. 组合总和
// 选择列表中元素无重复,但是可以重复选
/**
* 思路: 回溯
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* 函数一个是选择列表, 一个startIndex, 一个是targetSum
* startIndex用于记录本层递归中,集合从哪里开始遍历, 由于可以重复选择元素, 所以下一层递归应该还是从当前元素的下标开始
*
* 2. 回溯函数终止条件
* targetSum == 0时,保存起来,结束本层递归
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始
*/
/*List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backtrack(candidates,0, target);
return res;
}
private void backtrack(int[] candidates, int startIndex, int targetSum) {
if(targetSum < 0) {
return;
}
if(targetSum == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
path.add(candidates[i]);
backtrack(candidates, i, targetSum - candidates[i]);
path.remove(path.size()-1);
}
}*/
// 剪枝优化
// 排序 + 遍历范围优化
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backtrack(candidates,0, target);
return res;
}
private void backtrack(int[] candidates, int startIndex, int targetSum) {
if(targetSum < 0) {
return;
}
if(targetSum == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length && targetSum - candidates[i] >= 0; i++) {
path.add(candidates[i]);
backtrack(candidates, i, targetSum - candidates[i]);
path.remove(path.size()-1);
}
}
40. 组合总和II
/**
* 思路: 回溯 + 剪枝优化(排序、遍历过程)
*
* 先对数组进行排序
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(int[] candidates, int startIndex, int targetSum, boolean[] visited)
* candidates: 选择列表
* startIndex: 用于记录本层递归中,集合从哪里开始遍历
* targetSum: 目标和
* visited: 访问数组,判断同层节点是否已经遍历过
*
* 2. 回溯函数终止条件
* targetSum < 0时, 不符合条件, 提前返回
* targetSum == 0时,保存起来,结束本层递归
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始, 因为是排序数组,所以如果遍历到i时, targetSum已经小于0,则后面就不用再遍历了
*
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates == null || candidates.length == 0 || target == 0) {
return res;
}
// 访问数组,判断同层节点是否已经遍历过
boolean[] visited = new boolean[candidates.length];
Arrays.sort(candidates);
backtrack(candidates, 0, target, visited);
return res;
}
private void backtrack(int[] candidates, int startIndex, int targetSum, boolean[] visited) {
if(targetSum < 0) {
return;
}
if(targetSum == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length && targetSum - candidates[i] >= 0; i++) {
// 同一树层有两个重复的元素,不可以重复被选取
if(i > 0 && candidates[i] == candidates[i-1] && !visited[i-1]) {
continue;
}
// 同一树枝有两个重复的元素,但visited[i-1]为true,可以重复选取
path.add(candidates[i]);
visited[i] = true;
backtrack(candidates, i+1, targetSum-candidates[i], visited);
visited[i] = false;
path.remove(path.size()-1);
}
}
分割问题
131. 分割回文串
/**
* 思路: 回溯
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(String s, int startIndex)
* s: 选择列表
* startIndex: 用于记录本层递归中,集合从哪里开始遍历
*
* 2. 回溯函数终止条件
* targetSum == s.length() 时,保存起来,结束本层递归
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始,如果满足回文子串的条件,则进入下一层递归
*
*/
List<List<String>> res = new ArrayList<>();
List<String> path = new ArrayList<>();
public List<List<String>> partition(String s) {
backtrack(s, 0);
return res;
}
private void backtrack(String s, int startIndex) {
if(startIndex == s.length()) {
res.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < s.length(); i++) {
String substring = s.substring(startIndex, i + 1);
// 是回文子串
if(isPalindrome(substring)) {
path.add(substring);
backtrack(s, i+1);
path.remove(path.size()-1);
}
}
}
// 判断是否是回文串
private boolean isPalindrome(String substring) {
int left = 0;
int right = substring.length()-1;
while(left < right) {
if(substring.charAt(left) == substring.charAt(right)) {
left++;
right--;
} else {
break;
}
}
return left >= right;
}
93. 复原IP地址
/**
* 思路: 回溯
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(String s, int startIndex, int dotNum)
* s: 选择列表
* startIndex: 用于记录本层递归中,集合从哪里开始遍历
* dotNum: 句点的数量
*
* 2. 回溯函数终止条件
* dotNum == 3时, 做进一步判断
* (要注意第四段的下标是否越界)
* 如果第四段子串是合法的,则将第四段添加到路径中,最后添加到结果集res中,添加完毕后还要进行一步回溯操作,剔除刚刚添加的第4段
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始,如果子串是合法的,则进入下一层递归
* 若不合法,则提前结束本层循环
*
*/
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
public List<String> restoreIpAddresses(String s) {
if(s == null || s.length() == 0) {
return res;
}
// 超过12个数字无法组成IP地址
if(s.length() > 12) {
return res;
}
backtrack(s, 0, 0);
return res;
}
private void backtrack(String s, int startIndex, int dotNum) {
// 当出现 0.10.0.时, 即出现3个'.'时, 就进入判断
if(dotNum == 3) {
// 判断第四段子字符串是否合法,合法就放入res中
// 要注意第四段子字符串下标是否已经超过s字符串的大小!!!!
if(startIndex < s.length() && isValid(s.substring(startIndex, s.length()))) {
// 满足则添加最后一段,然后添加到结果集中
res.add(path.append(s.substring(startIndex, s.length())).toString());
// 添加完0.10.0.10后, 要记得回溯, 即删除最后的"10", 回退到只有3段的时候, 即0.10.0.
String[] split = path.toString().split("\\\\."算法 ---- LeetCode回溯系列问题题解