【算法】 ---- LeetCode回溯系列问题题解

本文详细介绍了回溯法的概念、应用场景,包括组合问题、切割问题、子集问题、排列问题和棋盘问题,并通过LeetCode上的经典题目如组合、组合总和、电话号码的字母组合、解数独等进行深入解析,展示了回溯法的模板和剪枝优化技巧。
摘要由CSDN通过智能技术生成

回溯

1. 什么是回溯?

回溯法也叫回溯搜索法,是一种搜索的方式

2. 回溯法解决的问题

  • 组合问题: N个数里面按一定规则找出k个数的集合
  • 切割问题: 一个字符串按一定规则有几种切割方式
  • 子集问题: 一个N个数的集合里有多少符合条件的子集
  • 排列问题: N个数按一定规则全排列,有几种排列方式
  • 棋盘问题: N皇后,解数独等等

组合是不强调元素顺序,排列强调元素顺序
例如: {1,2}和{2,1}在组合上,是一个集合。
对排列来说, {1,2}和{2,1}就是两个集合了

3. 回溯法的模板

  • 回溯函数模板返回值以及参数

    返回值一般为void,参数一开始不能一次性确定,所以一般先写逻辑,然后需要什么参数,就填充什么参数

    void backtrack(参数)
    
  • 回溯函数终止条件

    一般来说搜到叶子节点,即找到满足条件的一条答案,就把答案存放起来,并结束本层递归。

    if (终止条件) {
    	存放结果;
    	return;
    }
    
  • 回溯搜索的遍历过程

    回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成树的深度

    image-20210618102913821

    for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    	处理节点;
    	backtrack(路径,选择列表); // 递归
    	回溯,撤销处理结果
    }
    

    for循环就是遍历集合区间,可以理解一个节点有多少孩子,for循环就执行多少次。

    (for循环横向遍历,backtrack纵向遍历)

综合,回溯算法模板如下:

void backtrack(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtrack(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

LeetCode题目列表

image-20210620024018059

组合问题

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值