回溯法
回溯
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("\\.");
// path的长度 - 最后"10"的长度 得到"10"的起始位置
path.delete(path.length() - split[split.length-1].length(), path.length());
}
return;
}
for (int i = startIndex; i < s.length(); i++) {
String substring = s.substring(startIndex, i + 1);
if(isValid(substring)) {
// 满足条件的,添加到路径中
path.append(substring + ".");
// 递归进入下一层, dotNum数量要+1
backtrack(s, i+1, dotNum+1);
// 回溯,删除 0.10.0. 中的 0. 回退到上一步的 0.10.
String[] split = path.deleteCharAt(path.length() - 1).toString().split("\\.");
path.delete(path.length() - split[split.length-1].length(), path.length());
} else {
// 本层不符合的时候 提前结束本层循环
break;
}
}
}
private boolean isValid(String substring) {
// 只有一个'0'是合法的
if(substring.length() == 1) {
return true;
}
// "00" "000": 0开头的数字不合法
if(substring.charAt(0) == '0') {
return false;
}
int num = 0;
// 防止大数溢出,提前判断是否大于255
for (int i = 0; i < substring.length(); i++) {
num = num * 10 + (substring.charAt(i) - '0');
if(num > 255) {
return false;
}
}
return true;
}
子集问题
78. 子集
/**
* 思路: 回溯
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(int[] nums, int startIndex)
* nums: 选择列表
* startIndex: 用于记录本层递归中,集合从哪里开始遍历
*
* 2. 回溯函数终止条件
* startIndex == nums.length时
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始
*
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
private void backtrack(int[] nums, int startIndex) {
res.add(new ArrayList<>(path));
if(startIndex == nums.length) {
return;
}
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);
backtrack(nums, i+1);
path.remove(path.size()-1);
}
}
90. 子集II
/**
* 思路: 回溯
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(int[] nums, boolean[] used, int startIndex)
* nums: 选择列表
* used: 访问数组,判断同层节点是否已经遍历过
* startIndex: 用于记录本层递归中,集合从哪里开始遍历
*
* 2. 回溯函数终止条件
* startIndex == nums.length时
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始
*
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
backtrack(nums, used, 0);
return res;
}
private void backtrack(int[] nums, boolean[] used, int startIndex) {
res.add(new ArrayList<>(path));
if(startIndex == nums.length) {
return;
}
for (int i = startIndex; i < nums.length; i++) {
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) {
continue;
}
path.add(nums[i]);
used[i] = true;
backtrack(nums, used, i+1);
used[i] = false;
path.remove(path.size()-1);
}
}
排列问题
46. 全排列
// 排列元素不重复问题,不需要startIndex变量,需要used数组记录path里放了哪些元素
/**
* 思路: 回溯
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(int[] nums, boolean[] used)
* nums: 选择列表
* used: 记录path哪些元素使用了
*
* 2. 回溯函数终止条件
* path.size() == nums.length时
*
* 3. 单层搜索的过程
* for循环每次从0开始
*
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
backtrack(nums, used);
return res;
}
private void backtrack(int[] nums, boolean[] used) {
if(path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if(!used[i]) {
path.add(nums[i]);
used[i] = true;
backtrack(nums, used);
used[i] = false;
path.remove(path.size()-1);
}
}
}
47. 全排列II
// 排列元素重复问题,不需要startIndex变量,需要used数组记录path里放了哪些元素(同一树枝被使用过), 排重同一树层相同元素
/**
* 思路: 回溯
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(int[] nums, boolean[] used)
* nums: 选择列表
* used: 记录path哪些元素使用了
* - used[i-1] == false: 同一树层nums[i-1]使用过
* - used[i] == true: 同一树枝nums[i]没被使用过开始处理
*
* 2. 回溯函数终止条件
* path.size() == nums.length时
*
* 3. 单层搜索的过程
* for循环每次从0开始
*
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
backtrack(nums, used);
return res;
}
private void backtrack(int[] nums, boolean[] used) {
if(path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 同一树层: 排除同层重复的元素
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) {
continue;
}
// 同一树枝: 未被使用过的元素
if(!used[i]) {
path.add(nums[i]);
used[i] = true;
backtrack(nums, used);
used[i] = false;
path.remove(path.size()-1);
}
}
}
棋盘问题
51. N皇后
/**
* 思路: 回溯
*
* 定义一个全局变量, 用来存放符合条件结果的集合
*
* 1. 递归函数的返回值以及参数
* backtrack(int n, int row, char[][] chessboard)
* n: 棋盘的大小
* row: 遍历到棋盘的第几层
* chessboard: 棋盘默认情况
*
* 2. 回溯函数终止条件
* 递归到棋盘最底层,即叶子节点的时候,收集结果并返回
*
* 3. 单层搜索的过程
* for循环每次从col开始(0到n),然后递归深度即row控制棋盘的行
*
*/
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] chars : chessboard) {
Arrays.fill(chars, '.');
}
backtrack(n, 0, chessboard);
return res;
}
private void backtrack(int n, int row, char[][] chessboard) {
if(row == n) {
res.add(ArraysToList(chessboard));
return;
}
for (int col = 0; col < n; col++) {
// 检查当前要添加的元素是否合法
if(valid(row, col, chessboard)) {
chessboard[row][col] = 'Q';
backtrack(n, row+1, chessboard);
// 回溯,撤销皇后
chessboard[row][col] = '.';
}
}
}
/**
* 校验当前的位置是否合法, 排除行(忽略)、列、对角线(45°,135°)皇后的影响
*/
private boolean valid(int row, int col, char[][] chessboard) {
// 检查列是否有Q
for (int i = row-1; i >= 0; i--) {
if(chessboard[i][col] == 'Q') {
return false;
}
}
// 检查对角线45°
for (int i = row-1, j = col-1; i >= 0 && j >= 0; i--, j--) {
if(chessboard[i][j] == 'Q') {
return false;
}
}
// 检查对角线135°
for (int i = row-1, j = col + 1; i >= 0 && j < chessboard.length; i--, j++) {
if(chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
private List<String> ArraysToList(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] chars : chessboard) {
list.add(String.valueOf(chars));
}
return list;
}
52. N皇后II
/**
* 思路: 回溯
*
* 定义一个全局变量, 用来存放符合条件结果的集合
*
* 1. 递归函数的返回值以及参数
* backtrack(int n, int row, char[][] chessboard)
* n: 棋盘的大小
* row: 遍历到棋盘的第几层
* chessboard: 棋盘默认情况
*
* 2. 回溯函数终止条件
* 递归到棋盘最底层,即叶子节点的时候,收集结果并返回
*
* 3. 单层搜索的过程
* for循环每次从col开始(0到n),然后递归深度即row控制棋盘的行
*
*/
List<List<String>> res = new ArrayList<>();
public int totalNQueens(int n) {
char[][] chessBoard = new char[n][n];
for (char[] chars : chessBoard) {
Arrays.fill(chars, '.');
}
backtrack(n, 0, chessBoard);
return res.size();
}
private void backtrack(int n, int row, char[][] chessBoard) {
if(row == n) {
res.add(ArraysToList(chessBoard));
return;
}
for (int col = 0; col < n; col++) {
if(valid(row, col, chessBoard)) {
chessBoard[row][col] = 'Q';
backtrack(n, row + 1, chessBoard);
chessBoard[row][col] = '.';
}
}
}
private boolean valid(int row, int col, char[][] chessBoard) {
// 检查列是否有皇后'Q'
for (int i = row-1; i >= 0; i--) {
if(chessBoard[i][col] == 'Q') {
return false;
}
}
// 检查对角线45°是否有皇后'Q'
for (int i = row-1, j = col-1; i >= 0 && j >= 0; i--, j--) {
if(chessBoard[i][j] == 'Q') {
return false;
}
}
// 检查对角线135°是否有皇后'Q'
for (int i = row-1, j = col+1; i >= 0 && j < chessBoard.length; i--, j++) {
if(chessBoard[i][j] == 'Q') {
return false;
}
}
return true;
}
private List<String> ArraysToList(char[][] chessBoard) {
List<String> list = new ArrayList<>();
for (char[] chars : chessBoard) {
list.add(String.valueOf(chars));
}
return list;
}
37. 解数独
/**
* 思路: 回溯法
*
* 1. 递归函数的返回值的参数类型
* 返回值类型为boolean,一旦找到一个符合条件立刻就返回,相当于找到从根节点到叶子节点的唯一路径
*
* backtrack(char[][] board)
* 参数类型为棋盘
*
* 2. 递归终止条件
* 无需终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
*
* 没终止条件不会造成死循环问题嘛?
* 不会造成死循环,等数填满了棋盘自然就终止
*
* 3. 单层递归逻辑
* 树状图中我们需要的是一个二维的递归(两个for循环嵌套)
* 一层遍历棋盘的行,一层遍历棋盘的列,一行一列确定下来后,递归遍历这个位置放9个数组的可能性
*
*
*/
public void solveSudoku(char[][] board) {
// 二维递归
backtrack(board);
}
// 返回值是布尔类型,遇到满足的情况下直接返回
private boolean backtrack(char[][] board) {
// 二维递归, 双重for循环, 一层遍历棋盘的行,一层遍历棋盘的列,一行一列确定下来后,递归遍历这个位置放9个数组的可能性
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
if(board[row][col] != '.') {
continue;
}
// (row,col)这个位置放k是否合适
for (char k = '1'; k <= '9'; k++) {
if(valid(row, col, k, board)) {
// 满足情况下,进行选择
board[row][col] = k;
// 找到一组合适的立即返回
if(backtrack(board)) {
return true;
}
// 回溯
board[row][col] = '.';
}
}
// 若当前这个位置'1'~'9'都不满足,则返回false,回退到上一层
return false;
}
}
// 遍历结束没有返回false,说明找到了合适的棋盘位置了
return true;
}
private boolean valid(int row, int col, char val, char[][] board) {
// 检查行是否重复
for (int j = 0; j < 9; j++) {
if(board[row][j] == val) {
return false;
}
}
// 检查列是否重复
for (int i = 0; i < 9; i++) {
if(board[i][col] == val) {
return false;
}
}
// 检查9个格子是否重复
// 比如当前位置是(8,8), 则检查从start = 8 / 3 * 3 = 6, 下标为6开始检查
int startRow = row / 3 * 3;
int startCol = col / 3 * 3;
for (int i = startRow; i < startRow + 3; i++) {
for (int j = startCol; j < startCol + 3; j++) {
if(board[i][j] == val) {
return false;
}
}
}
return true;
}
其它问题
491. 递增子序列
/**
* 思路: 回溯
*
* 1. 递归函数的返回值以及参数
* 定义两个全局变量, 一个是存放符合条件的单一结果, 一个用来存放符合条件结果的集合
*
* backtrack(int[] nums, int startIndex)
* nums: 选择列表
* startIndex: 用于记录本层递归中,集合从哪里开始遍历
*
* 2. 回溯函数终止条件
* startIndex == nums.length时
*
* 3. 单层搜索的过程
* for循环每次从startIndex开始
* 由于本题是无序排列的,所以要在每一层添加一个set集合,用于排除重复, 比如选择列表中[4,7,7], 最后一个元素7不应该考虑
*
*/
/*List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
if(nums == null || nums.length <= 1) {
return res;
}
backtrack(nums, 0);
return res;
}
private void backtrack(int[] nums, int startIndex) {
if(path.size() >= 2)
res.add(new ArrayList<>(path));
if(startIndex == nums.length) {
return;
}
Set<Integer> set = new HashSet<>(); // 对本层元素进行去重
for (int i = startIndex; i < nums.length; i++) {
// 不符合条件的跳过本层循环
// path不为空,如果当前元素小于路径中的最后一个元素,则结束本轮循环
// 如果本层set已经包含了nums[i],那么也结束本层循环
if(!path.isEmpty() && nums[i] < path.get(path.size()-1) || set.contains(nums[i])) {
continue;
}
set.add(nums[i]); // 记录这个元素已经使用过了,本层后面就不能使用了
path.add(nums[i]);
backtrack(nums, i+1);
path.remove(path.size()-1);
}
}*/
// 优化
// 用一个数组 代替 HashMap
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
if(nums == null || nums.length <= 1) {
return res;
}
backtrack(nums, 0);
return res;
}
private void backtrack(int[] nums, int startIndex) {
if(path.size() >= 2)
res.add(new ArrayList<>(path));
if(startIndex == nums.length) {
return;
}
int[] used = new int[201]; // 因为整数的范围是[-100,100] 对本层元素进行去重
for (int i = startIndex; i < nums.length; i++) {
// 不符合条件的跳过本层循环
// path不为空,如果当前元素小于路径中的最后一个元素,则结束本轮循环
// 如果本层set已经包含了nums[i],那么也结束本层循环
if(!path.isEmpty() && nums[i] < path.get(path.size()-1) || used[nums[i] + 100] == 1) {
continue;
}
used[nums[i] + 100] = 1; // 记录这个元素已经使用过了,本层后面就不能使用了
path.add(nums[i]);
backtrack(nums, i+1);
path.remove(path.size()-1);
}
}
332. 重新安排行程
/**
* 思路: 回溯
* 1. 递归函数的返回值和参数
* 返回值是boolean类型, 只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线
* boolean backtrack(int ticketNum)
* 参数为机票数,表示有多少个航班
*
* 2. 递归终止条件
* 航班数 + 1 = 结果数(遇到的机场个数)
*
* 3. 单层搜索的逻辑
*
* 以出发机场所到达的所有机场,进行遍历到达机场
*
* Map<String, Map<String, Integer>> map 作为机场之间映射关系
* key:出发机场, value: key为到达机场, value为航班次数
* 其中 Map<String, Integer> 类型应该为TreeMap,按照key即到达机场的字母进行排序
*
* 输入: ["JFK","KUL"], ["JFK", "NRT"], ["NRT", "JFK"]
*
* JFK 飞 {KUL, NRT}
* /\
* KUL / \ NRT
* / \
* KUL飞{} NRT飞{JFK}
* 行程:JFK->KUL \
* 3+1 != 2 \ 行程:JFK->NRT
* \
* JFK飞{KUL, NRT}
* / \
* 飞KUL / \ 飞NRT(×),次数已用完
* / \
* KUL飞{}
* 行程: JFK->NRT->JFK->KUL
* 3 + 1 = 4
* 机票数 + 1 = 结果集
*
*/
List<String> res = new ArrayList<>();
// key:出发机场, value: key为到达机场, value为航班次数
Map<String, Map<String, Integer>> map = new HashMap<>();
public List<String> findItinerary(List<List<String>> tickets) {
if(tickets == null || tickets.size() == 0) {
return new ArrayList<>();
}
// 例子: ["JFK", "KUL"], ["JFK","NRT"], ["NRT", "JFK"]
for (List<String> ticket : tickets) {
// 第二次ticket
// ticket = ["JFK", "NRT"]
if(map.containsKey(ticket.get(0))) {
// 获取到temp,此时内容为{["KUL", 1]}
Map<String, Integer> temp = map.get(ticket.get(0));
// 存放"NRT"后, 此时temp内容为{["KUL", 1], ["NRT", 1]}
temp.put(ticket.get(1), temp.getOrDefault(ticket.get(1), 0) + 1);
// 重新放入map中
map.put(ticket.get(0), temp);
} else {
// 第一次ticket
// ticket = ["JFK", "KUL"]
// 不存在出发机场key时,新创建key, value为TreeMap,按照value的key字母排序
// "KUL":1
Map<String, Integer> temp = new TreeMap<>();
temp.put(ticket.get(1), 1);
map.put(ticket.get(0), temp);
}
}
// 第一个出发地
res.add("JFK");
backtrack(tickets.size());
return res;
}
// ticketNum参数表示有多少个航班(终止条件会用到)
// 注意终止条件返回boolean,和其它回溯法的返回值不一样
private boolean backtrack(int ticketNum) {
// 递归终止条件
if(ticketNum + 1 == res.size()) {
return true;
}
// 结果集中的最后一个元素作为下一次的 出发机场
String last = res.get(res.size()-1);
// map集合中存在出发机场的key才进入选择
if(map.containsKey(last)) {
// 循环遍历key出发机场对应的所有的到达机场
for (Map.Entry<String, Integer> target : map.get(last).entrySet()) {
// 获取当前到达机场的航班次数
int count = target.getValue();
// 去重
// 比如JFK -> NRT -> JFK -> (KUL, NRT(不能飞,次数用完了))
if(count > 0) {
// 到达机场添加到结果集中
res.add(target.getKey());
// 对应航班次数减1
target.setValue(count-1);
if(backtrack(ticketNum)) {
return true;
}
// 回溯
// 撤销到达机场
// 撤销到达机场对应的航班次数+1
res.remove(res.size()-1);
target.setValue(count);
}
}
}
// 比如JFK -> KUL 无法再继续飞下去了, 因为KUL没有作为任何的航班的出发机场,所以返回false
return false;
}