回溯基础
- 问题:组合、排列、分割、子集、棋盘;
- 回溯法解决的问题都可以抽象为树形结构;要能画出来
- 回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度
- 模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
排列、子集、组合
- 元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式;
- 元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次;
- 元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次。
- 若下层中间节点的集合大小与上层一致,说明题目的要求是可重复选;
- 如果是组合或者子集问题,同层相邻中间节点的集合逐渐缩小(for循环,记录startIndex)。
1、组合
77. 组合(元素无重不可复选)
n个数中取k个数的组合
- 直接的解法当然是使用for循环,例如示例中k为2,很容易想到 用两个for循环;
- 如果n为100,k为50呢,那就50层for循环。用for循环嵌套连暴力都写不出来;
- 递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了。
- 递归的层数。例如:n为100,k为50的情况下,就是递归50层。
- 基本思路
- n相当于树的宽度,k相当于树的深度;
- 每次搜索到了叶子节点,我们就找到了一个结果
for循环剪枝
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTracing(n,k,1);
return res;
}
public void backTracing(int n, int k, int startIndex){
if(path.size() == k){
res.add(new ArrayList<>(path));
return ;
}
for(int i = startIndex; i <= n - (k-path.size())+1; i++){
path.add(i);
backTracing(n,k,i+1);
path.remove(path.size() - 1);
}
}
}
216. 组合总和 III(元素无重不可复选)
回溯+元素个数剪枝(for循环)+元素大小剪枝(循环内)
剪枝:
- 元素总和如果已经大于n;
- for循环的范围。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int sum = 0;
public List<List<Integer>> combinationSum3(int k, int n) {
backTracing(k,n,1,9);
return res;
}
public void backTracing(int k, int n, int l, int r){
if( path.size() == k){
if( sum == n) res.add(new ArrayList<>(path));
return ;
}
for(int i = l; i <= r - (k - path.size()) + 1; i++){
if(sum + i > n) return; //元素有序,i后面的元素+sum也都比n大,可以剪枝
sum+=i;
path.add(i);
backTracing(k,n,i+1,r);
sum-=i;
path.remove(path.size() - 1);
}
}
}
17. 电话号码的字母组合
class Solution {
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return res;
}
//初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
backTracing(digits,numString,0);
return res;
}
public void backTracing(String digits, String[] numString, int num){
if(digits.length() == num){
res.add(path.toString());
return ;
}
String str = numString[digits.charAt(num) - '0'];
for(int i = 0; i < str.length(); i++){
path.append(str.charAt(i));
backTracing(digits,numString,num+1);
path.deleteCharAt(path.length() - 1);
}
}
}
39. 组合总和(元素无重可复选)
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates == null || candidates.length == 0 ) return res;
backTracing(candidates,target,0);
return res;
}
public void backTracing(int[] candidates, int target, int startIndex){
if(sum > target) return ;
if(sum == target){
res.add(new LinkedList<>(path));
return ;
}
for(int i = startIndex; i < candidates.length; i++){
sum+=candidates[i];
path.add(candidates[i]);
backTracing(candidates,target,i);
sum-=candidates[i];
path.removeLast();
}
}
}
40.组合总和II(元素可重不可复选)
- 同一个组合内是可以重复的,但两个组合不能相同。如[1(a),1(b),1(c ),2],组合内可以取深度为3的路径[1(a),1(b),2],同一层相邻组合不能再取[1(b),1(c ),2];
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates == null && candidates.length == 0) return res;
Arrays.sort(candidates);
backTracing(candidates,target,0);
return res;
}
public void backTracing(int[] candidates,int target,int startIndex){
if(sum == target){
res.add(new LinkedList<>(path));
return ;
}
//i = startIndex控制组合树同层,之前选过的元素不能再选
for(int i = startIndex; i < candidates.length; i++){
//剪枝
if(sum + candidates[i] > target) return ;
//树层去重(针对有集合中有重复元素的情况)
if(i > startIndex && candidates[i-1]==candidates[i]) continue;
path.add(candidates[i]);
sum+=candidates[i];
//i+1控制不可重复选
backTracing(candidates,target,i+1);
path.removeLast();
sum-=candidates[i];
}
}
}
2、分割
131.分割回文串
- 切割问题可以抽象为组合问题
- 如何模拟那些切割线:startIndex来表示
- 切割问题中递归如何终止:切割线到达字符末端
- 在递归循环中如何截取子串:startIndex为闭区间起点,i为闭区间终点
- 如何判断回文:两端往中间循环,判断是否相等
- 切割过的地方不能重复切割所以递归函数需要传入i + 1
class Solution {
List<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
if(s == null || s.length() == 0) return res;
backTracing(s, 0);
return res;
}
public void backTracing(String s, int startIndex){
if( startIndex >= s.length()){
res.add(new LinkedList<>(path));
return;
}
for(int i = startIndex; i < s.length(); i++){
if(!isPalindrome(s,startIndex, i)) continue;
path.add(s.substring(startIndex, i+1));
backTracing(s, i+1);
path.removeLast();
}
}
public boolean isPalindrome(String s,int l, int r){
for(int i = l, j = r; i < j; i++, j--){
if(s.charAt(i) != s.charAt(j)) return false;
}
return true;
}
}
93. 复原 IP 地址
class Solution {
List<String> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<String> restoreIpAddresses(String s) {
if(s == null || s.length() == 0) return res;
backTracing(s,0);
return res;
}
public void backTracing(String s, int startIndex){
//已经存在3个点,后续直接判断最后一段数字是否有效
if(path.size() == 3){
if(isValid(s,startIndex,s.length()-1))
res.add(String.join(".",path) + "." + s.substring(startIndex, s.length()));
return ;
}
for(int i = startIndex; i < s.length(); i++){
if(!isValid(s,startIndex,i)) continue ;
path.add(s.substring(startIndex,i+1));
backTracing(s,i+1);
path.removeLast();
}
}
//表示左闭右闭区间
public boolean isValid(String s, int l, int r){
//1、r<l,代表区间内已经没有元素
//2、r > l && '0' == s.charAt(l),代表以0开头且后续存在数字
//3、r - l > 2,代表区间长度大于3,肯定超过255
if(r < l || r > l && '0' == s.charAt(l) || r - l > 2) return false;
int num = Integer.valueOf(s.substring(l,r+1));
//4、数字不在[0,255]中
if( num < 0 || num > 255) return false;
return true;
}
}
3、子集
78. 子集(元素无重不可复选)
- 子集问题的解是树的所有节点
- 子集问题的解是无序的,跟组合一样,所以也要用startIndex划分
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracing(nums, 0);
return res;
}
public void backTracing(int[] nums, int startIndex){
res.add(new LinkedList<>(path));
if(startIndex>= nums.length) return ;
for(int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
backTracing(nums,i+1);
path.removeLast();
}
}
}
90. 子集 II
- 同层的相同值,会产生重复解
- 注意要先排序,想值相同的元素相邻
- 正确的树形结构
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backTracing(nums,0);
return res;
}
public void backTracing(int[] nums,int startIndex){
res.add(new LinkedList<>(path));
if(startIndex >= nums.length) return ;
for(int i = startIndex; i < nums.length; i++){
if(i > startIndex && nums[i] == nums[i-1]) continue;
path.add(nums[i]);
backTracing(nums,i+1);
path.removeLast();
}
}
}
491. 递增子序列
- 要求子序列递增才符号解的条件,所以不能排序;
- 不能排序意味着要之前排序后相邻元素相等的去重逻辑失效,要使用新的去重逻辑;
- 题目中数组元素存在范围限制,可以直接用数组作字典来判断某个元素在同一父节点下的同层是否使用过;
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracing(nums,0);
return res;
}
public void backTracing(int[] nums, int startIndex){
if(path.size() > 1) res.add(new LinkedList<>(path));
if(startIndex>= nums.length) return ;
int[] used = new int[201];
for(int i = startIndex; i < nums.length; i++){
if( path.size() > 0 && path.getLast() > nums[i]||used[nums[i]+100] == 1) continue;
used[nums[i]+100] = 1;
path.add(nums[i]);
backTracing(nums,i+1);
path.removeLast();
}
}
}
排列
46. 全排列
- 不用startIndex来标记同一父节点的子节点循环起点,因为这是排列,存在顺序;
- 使用used数组标记nums数组每个位置的元素是否使用过
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
int[] used = new int[nums.length];
backTracing(nums,used);
return res;
}
public void backTracing(int[] nums, int[] used){
if(nums.length == path.size()){
res.add(new LinkedList<>(path));
return ;
}
for(int i = 0; i < nums.length; i++){
if(used[i] == 1) continue;
path.add(nums[i]);
used[i] = 1;
backTracing(nums,used);
path.removeLast();
used[i] = 0;
}
}
}
47. 全排列 II
- 使用used数组标记nums数组每个位置的元素是否使用过
由于元素可以重复,还需要使用used数组来去重。首先对nums数组进行排序,随后判断(i > 0 && nums[i] == nums[i-1] && used[i-1] == 0)时,说明相邻元素重复,可以跳过
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
int[] used = new int[nums.length];
backTracing(nums,used);
return res;
}
public void backTracing(int[] nums,int[] used){
if(nums.length == path.size()){
res.add(new LinkedList<>(path));
return ;
}
for(int i = 0; i < nums.length; i++){
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == 0) continue;
if(used[i] == 1) continue;
path.add(nums[i]);
used[i] = 1;
backTracing(nums,used);
path.removeLast();
used[i] = 0;
}
}
}
- 使用used数组标记nums数组每个位置的元素是否使用过
使用flag数组标记nums数组中每个元素是否使用过
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
//判断nums数组某个元素位置上的元素是否使用过,用于排列中树枝上避免某个元素重复使用
int[] used = new int[nums.length];
backTracing(nums,used);
return res;
}
public void backTracing(int[] nums,int[] used){
if(nums.length == path.size()){
res.add(new LinkedList<>(path));
return ;
}
//判断元素在同一父节点的同层是否重复使用
int[] flag = new int[21];
for(int i = 0; i < nums.length; i++){
if(flag[nums[i]+10] == 1) continue;
if(used[i] == 1) continue;
flag[nums[i]+10] = 1;
path.add(nums[i]);
used[i] = 1;
backTracing(nums,used);
path.removeLast();
used[i] = 0;
}
}
}
棋盘
51. N皇后
- 判断落子是否有效的逻辑;
依据斜率和截距写出函数;
对角线:c = y - x ,因为截距可能为负数,故使用 y - x + n;
反对象线:b = y + x。
class Solution {
List<List<String>> res = new ArrayList<>();
boolean[] col;
boolean[] dg;
boolean[] udg;
public List<List<String>> solveNQueens(int n) {
col = new boolean[n];
dg = new boolean[2*n];
udg = new boolean[2*n];
char[][] chessboard = new char[n][n];
for(int i = 0; i<n; i++){
for(int j = 0; j < n; j++){
chessboard[i][j] = '.';
}
}
backTracing(chessboard,n,0);
return res;
}
public void backTracing(char[][] chessboard,int n, int row){
if(row == n){
List<String> tmp = new ArrayList<>();
for(char[] r: chessboard){
tmp.add(new String(r));
}
res.add(tmp);
return ;
}
for(int i = 0; i < n; i++){
if( col[i] || dg[i - row + n] || udg[i + row]) continue;
col[i] = dg[i - row + n] = udg[i+row] = true;
chessboard[row][i] = 'Q';
backTracing(chessboard,n,row+1);
chessboard[row][i] = '.';
col[i] = dg[i - row + n] = udg[i+row] = false;
}
}
}
37. 解数独
- 使用boolean返回值,找到一个结果后就直接返回
class Solution {
public void solveSudoku(char[][] board) {
backTracing(board);
}
public boolean backTracing(char[][] board){
//遍历行
for(int i = 0; i < board.length; i++){
//遍历列
for(int j = 0; j < board[0].length; j++){
//跳过原始数字
if(board[i][j] == '.'){
//枚举可能的数字
for(char k= '1'; k <= '9'; k++){
//是否有效
if(isValid(board,i,j,k)){
board[i][j] = k;
//找到一组解立即返回
if(backTracing(board)) return true;
board[i][j] = '.';
}
}
//该位置9个数字都试过了不行,返回false,可能是之前的填错了,或者根本就无解
return false;
}
}
}
// 遍历完没有空格后,也没有return false,说明找到一组解
return true;
}
public boolean isValid(char[][] board, int r, int l,char val){
for(int i = 0; i < board.length; i++){
if(board[r][i] == val) return false;
if(board[i][l] == val) return false;
}
int startrow = r / 3 * 3;
int startcol = l / 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;
}
}