算法训练--回溯

479 阅读12分钟

算法训练--回溯

回溯概念

  • Backtracking

  • 回溯法采用试错的思想,它尝试分步的去解决一个问题,在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至上几步的计算,再通过其他可能的分步解答再次尝试寻找问题的答案

    回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

    1. 找到一个可能存在的正确答案
    2. 在尝试了所有可能的分步法后宣告该问题没有答案

    在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算

与穷举的联系

  • 回溯法简单来说就是按照深度优先的顺序,穷举所有可能性的算法,但是回溯算法比暴力穷举法更高明的地方就是回溯算法可以随时判断当前状态是否符合问题的条件,一旦不符合条件,那么就退回到上一个状态,省去了继续往下探索的时间
  • 回溯法的特点是深度优先遍历,也就是该问题的遍历顺序是1->2->3,然后从子节点3返回,从子节点2返回,再到1->3->2,以此类推;状态的返回只有当前的节点不再满足问题的条件或者我们已经找到了问题的一个解时,才会返回,否则会以深度优先一直在解空间树内遍历下去

剪枝

  • 针对这类问题的优化策略:剪枝以及启发式搜索

    所谓剪枝优化,就是判断当前的分支树是否符合问题的条件,如果当前分支树不符合条件,那么就不再遍历这个分支里的所有路径

    所谓启发式搜索指的是,给回溯法搜索子节点的顺序设定一个优先级,从该子节点往下遍历更有可能找到问题的解

回溯法的效率

  • 虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质

回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合

    可能分不清什么是组合,什么是排列?组合是不强调元素顺序的,排列是强调元素顺序

    例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了,记住组合无序,排列有序

  • 切割问题:一个字符串按一定规则有几种切割方式

  • 子集问题:一个N个数的集合里有多少符合条件的子集

  • 排列问题:N个数按一定规则全排列,有几种排列方式

  • 棋盘问题:N皇后,解数独等等

如何理解回溯法

  • 回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!

    因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度,递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)

一般步骤

  • 用回溯算法解决问题的一般步骤:

    1. 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解

    2. 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间

    3. 以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索

  • 回溯代码模板

    void backtracking(参数) {
        if (终止条件) {
            存放结果;
            return;
        }
    
        for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
            处理节点;
            backtracking(路径,选择列表); // 递归
            回溯,撤销处理结果
        }
    }
    

组合

77. 组合

  • 题目描述

    image.png

  • 题解

    把组合问题抽象为如下树形结构:

    77.组合

    可以看出这个棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不在重复取。

    第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。

    每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围

    图中可以发现n相当于树的宽度,k相当于树的深度

    那么如何在这个树上遍历,然后收集到我们要的结果集呢?

    图中每次搜索到了叶子节点,我们就找到了一个结果

    相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合

    path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径

    剪枝优化:

    1. 已经选择的元素个数:path.size();
    2. 还需要的元素个数为: k - path.size();
    3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> combine(int n, int k) {
            backtracking(n,k,1);
            return res;
        }
        public void backtracking(int n,int k, int cur){
          	//递归终止条件
            if(temp.size()==k){
                res.add(new ArrayList<>(temp));
                return;
            }
          	//剪枝优化
            for(int i=cur;i<=n-(k-temp.size())+1;i++){
              	//处理节点
                temp.add(i);
                backtracking(n,k,i+1);
              	//回溯,撤销处理的节点
                temp.remove(temp.size()-1);
            }
        }
    }
    

216. 组合总和 III

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> combinationSum3(int k, int n) {
            backtracking(k,n,1,0);
            return res;
        }
        public void backtracking(int k,int n,int startIndex,int sum){
            //剪枝操作
            if(sum>n) return;
            //递归终止条件
            if(temp.size()==k){
                if(n==sum){
                    res.add(new ArrayList<>(temp));
                    return;
                }
            }
            //剪枝
            for(int i=startIndex;i<=9-(k-temp.size())+1;i++){
                //处理
                temp.add(i);
                sum+=i;
                //注意i+1调整startIndex
                backtracking(k,n,i+1,sum);
                //回溯
                sum-=i;
                temp.remove(temp.size()-1);
            }
        }
    }
    

17. 电话号码的字母组合

  • 题目描述

    image.png

  • 题解

    这个num是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串)

    那么终止条件就是如果num 等于 输入的数字个数(digits.length())了(本来num就是用来遍历digits的)

    class Solution {
        List<String> res=new ArrayList<>();
        StringBuilder sb=new StringBuilder();
        public List<String> letterCombinations(String digits) {
            if(digits.length()==0) return res;
            //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
            String[] numArr={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
            backtracking(digits,numArr,0);
            return res;
        }
        //比如digits如果为"23",num 为0,则str表示2对应的 abc
        public void backtracking(String digits,String[] numArr,Integer num){
            if(digits.length()==num){
                res.add(sb.toString());
                return;
            }
            String current=numArr[digits.charAt(num)-'0'];
            for(char ch:current.toCharArray()){
                sb.append(ch);
                backtracking(digits,numArr,num+1);
                sb.deleteCharAt(sb.length()-1);
            }
        }
    }
    

39. 组合总和

  • 题目描述

    image.png

  • 题解

    本题元素为可重复选取的,在求和问题中,排序之后加剪枝是常见的套路

    • 组合没有数量要求
    • 元素可无限重复选取
    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> combinationSum(int[] candidates, int target) {
          	//排序,便于剪枝
            Arrays.sort(candidates);
            backtracking(candidates,target,0,0);
            return res;
        }
        public void backtracking(int[] candidates ,int target, int startIndex,int sum){
          	//剪枝
            if(sum>target) return;
            if(sum==target){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=startIndex;i<candidates.length;i++){
                temp.add(candidates[i]);
                sum+=candidates[i];
              	// 关键点:不用i+1了,表示可以重复读取当前的数
                backtracking(candidates,target,i,sum);
                sum-=candidates[i];
                temp.remove(temp.size()-1);
            }
        }
    }
    

40. 组合总和 II

  • 题目描述

    image.png

  • 题解

    本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合

    强调一下,树层去重的话,需要对数组排序!

    40.组合总和II

    如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]

    此时for循环里就应该做continue的操作

    40.组合总和II1

    • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
    • used[i - 1] == false,说明同一树层candidates[i - 1]使用过
    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        boolean[] used;
        public List<List<Integer>> combinationSum2(int[] candidates, int target) {
          	//为了将重复的数字都放到一起,所以先进行排序
            Arrays.sort(candidates);
          	//加标志数组,用来辅助判断同层节点是否已经遍历
            used=new boolean[candidates.length];
            backtracking(candidates,target,0,0);
            return res;
        }
        public void backtracking(int[] candidates,Integer target,Integer stratIndex,Integer sum){
            if(sum>target)return;
            if(sum==target){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=stratIndex;i<candidates.length;i++){
              	// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
        				// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
        				// 要对同一树层使用过的元素进行跳过
                if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;
              	if(sum+candidates[i]>target) break;
                temp.add(candidates[i]);
                used[i]=true;
                sum+=candidates[i];
              	//每个节点仅能选择一次,所以从下一位开始
                backtracking(candidates,target,i+1,sum);
                sum-=candidates[i];
                used[i]=false;
                temp.remove(temp.size()-1);
            }
        }
    }
    
  
  

## 分割

### [131. 分割回文串](https://leetcode-cn.com/problems/palindrome-partitioning/)

* 题目描述

  ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00583134e2aa40afb8d9436acec16599~tplv-k3u1fbpfcp-watermark.image?)

* 题解

  ```java
  class Solution {
      List<List<String>> res=new ArrayList<>();
      List<String> temp=new ArrayList<>();
      public List<List<String>> partition(String s) {
          backtraking(s,0);
          return res;
      }
      public void backtraking(String s,int startIndex){
        	//直到遍历到字符串末尾
          if(startIndex==s.length()){
              res.add(new ArrayList(temp));
              return;
          }
          for(int i=startIndex;i<s.length();i++){
              String str=s.substring(startIndex,i+1);
            	//如果是回文子串,则记录
              if(isReverse(str)){
                  temp.add(str);
                  //起始位置后移,保证不重复
                  backtraking(s,i+1);
                  temp.remove(temp.size()-1);
              }else{
                  continue;
          }
      }
  }
  		//判断是否是回文串
      public boolean isReverse(String s){
          int i=0,j=s.length()-1;
          while(i<j){
              if(s.charAt(i)!=s.charAt(j)){
                  return false;
              }
              i++;
              j--;
          }
          return true;
      }
  }
/**
	dp
*/
class Solution {
    List<List<String>> res=new ArrayList<>();
    List<String> temp=new ArrayList<>();
    int n;
    boolean[][] dp;
    public List<List<String>> partition(String s) {
        n=s.length();
        dp=new boolean[n][n];
        for(int i=0;i<n;i++){
            dp[i][i]=true;
        }
        char[] sArr=s.toCharArray();
        for(int r=0;r<n;r++){
            for(int l=0;l<n;l++){
                if(sArr[l]!=sArr[r]){
                    dp[l][r]=false;
                }else{
                    if(r-l<3){
                        dp[l][r]=true;
                    }else{
                        dp[l][r]=dp[l+1][r-1];
                    }
                }
            }
        }
        dfs(s,0);
        return res;
    }
    public void dfs(String s, int i){
        if(i==n){
            res.add(new ArrayList(temp));
            return;
        }
        for(int j=i;j<n;j++){
            if(dp[i][j]){
                temp.add(s.substring(i,j+1));
                dfs(s,j+1);
                temp.remove(temp.size()-1);
            }
        }
    }
}

93. 复原 IP 地址

  • 题目描述

    image.png

  • 题解

    切割问题就可以使用回溯搜索法把所有可能性搜出来

    93.复原IP地址

    class Solution {
        List<String> res=new ArrayList<>();
        public List<String> restoreIpAddresses(String s) {
            backtracking(s,0,0);
            return res;
        }
      	//startIndex: 搜索的起始位置,pointNum:添加逗点的数量
        public void backtracking(String s, int startIndex,int pointNum){
            if(pointNum==3){// 逗点数量为3时,分隔结束
                // 判断第四段子字符串是否合法,如果合法就放进result中
                if(isVaild(s,startIndex,s.length()-1)){
                    res.add(s);
                }
                return;
            }
            for(int i=startIndex;i<s.length();i++){
              	//判断 [startIndex,i] 这个区间的子串是否合法
                if(isVaild(s,startIndex,i)){
                    s=s.substring(0,i+1)+"."+s.substring(i+1);
                    pointNum++;
                  	//插入逗点之后下一个子串的起始位置为i+2
                    backtracking(s,i+2,pointNum);
                    pointNum--;
                    s=s.substring(0,i+1)+s.substring(i+2);
                }else{
                  	//不合法 直接退出循环
                    break;
                }
            }
        }
    		//判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法
        public boolean isVaild(String s,int start,int end){
            if(start>end){
                return false;
            }
          	//0开头的数字不合法
            if(s.charAt(start)=='0' && start!=end){
                return false;
            }
            int num=0;
            for(int i=start;i<=end;i++){
                num=num*10+(s.charAt(i)-'0');
              	//如果大于255了不合法
                if(num>255) return false;
            }
            return true;
        }
    }
    

子集

78. 子集

  • 题目描述

    image.png

  • 题解

    如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点

    那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始

    78.子集

    从图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> subsets(int[] nums) {
            backtracking(nums,0);
            return res;
        }
        public void backtracking(int[] nums,int startIndex){
          	//收集子集,要放在终止添加的上面,否则会漏掉自己
            res.add(new ArrayList(temp));
            if(startIndex>=nums.length){
                return;
            }
            for(int i=startIndex;i<nums.length;i++){
                temp.add(nums[i]);
                backtracking(nums,i+1);
                temp.remove(temp.size()-1);
            }
        }
    }
    

90. 子集 II

  • 题目描述

    image.png

  • 题解

    有重复元素,子集去重:注意去重需要先对集合排序

    90.子集II

    从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集!

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> subsetsWithDup(int[] nums) {
          	//先排序排序排序
            Arrays.sort(nums);
            boolean[] used=new boolean[nums.length];
            backtacking(nums,0,used);
            return res;
        }
        public void backtacking(int[] nums,int startIndex,boolean[] used){
            res.add(new ArrayList(temp));
            if(startIndex>=nums.length){
               return;
            }
            for(int i=startIndex;i<nums.length;i++){
               	// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
                // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
                // 而我们要对同一树层使用过的元素进行跳过
                if(i>0 && nums[i]==nums[i-1] && used[i-1]==false){
                    continue;
                }
                temp.add(nums[i]);
                used[i]=true;
                backtacking(nums,i+1,used);
                temp.remove(temp.size()-1);
                used[i]=false;
            }
        }
    }
    

排列

46. 全排列

  • 题目描述

    image.png

  • 题解

    46.全排列

    大家此时可以感受出排列问题的不同:

    • 每层都是从0开始搜索而不是startIndex
    • 需要used数组记录temp里都放了哪些元素了
    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        boolean[] used;
        public List<List<Integer>> permute(int[] nums) {
            used=new boolean[nums.length];
            backtracking(nums,used);
            return res;
        }
        public void backtracking(int[] nums,boolean[] used){
            if(temp.size()==nums.length){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=0;i<nums.length;i++){
                if(used[i]) continue;
                temp.add(nums[i]);
                used[i]=true;
                backtracking(nums,used);
                used[i]=false;
                temp.remove(temp.size()-1);
            }
        }
    }
    

47. 全排列 II

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        boolean[] used;
        public List<List<Integer>> permuteUnique(int[] nums) {
          	//去重 排序排序排序
            Arrays.sort(nums);
            used=new boolean[nums.length];
            backtracking(nums,used);
            return res;
        }
        public void backtracking(int[] nums,boolean[] used){
            if(temp.size()==nums.length){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=0;i<nums.length;i++){
              	//used[i - 1] == false,说明同一树层nums[i - 1]使用过
                //如果同一树层nums[i - 1]使用过则直接跳过
                if(i>0 && nums[i]==nums[i-1] && used[i-1]==false) continue;
              	//排列,选择过得元素就不再选择
                if(used[i]) continue;
                temp.add(nums[i]);
                used[i]=true;
                backtracking(nums,used);
                temp.remove(temp.size()-1);
                used[i]=false;
            }
        }
    }
    

棋盘问题

51. N 皇后

  • 题目描述

    image.png

  • 题解

    首先来看一下皇后们的约束条件:

    1. 不能同行
    2. 不能同列
    3. 不能同斜线

    51.N皇后

    class Solution {
        List<List<String>> res = new ArrayList<>();
        public List<List<String>> solveNQueens(int n) {
            char[][] chessboard = new char[n][n];
            for (char[] c : chessboard) {
                Arrays.fill(c, '.');
            }
            backTrack(n, 0, chessboard);
            return res;
        }
        public void backTrack(int n, int row, char[][] chessboard) {
            if (row == n) {
                res.add(Array2List(chessboard));
                return;
            }
            for (int col = 0;col < n; ++col) {
                if (isValid (row, col, n, chessboard)) {
                    chessboard[row][col] = 'Q';
                    backTrack(n, row+1, chessboard);
                    chessboard[row][col] = '.';
                }
            }
    
        }
        public List Array2List(char[][] chessboard) {
            List<String> list = new ArrayList<>();
    
            for (char[] c : chessboard) {
                list.add(String.copyValueOf(c));
            }
            return list;
        }
        public boolean isValid(int row, int col, int n, char[][] chessboard) {
            // 检查列
            for (int i=0; i<row; ++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<=n-1; i--, j++) {
                if (chessboard[i][j] == 'Q') {
                    return false;
                }
            }
            return true;
        }
    }
    

面试题 08.12. 八皇后

  • 题目描述

    image.png

  • 题解

    class Solution {
         List<List<String>> solutions = new ArrayList();
         Set<Integer> columns = new HashSet();
         Set<Integer> diagonals1 = new HashSet();
         Set<Integer> diagonals2 = new HashSet();
        public List<List<String>> solveNQueens(int n) {
            int[] queens = new int[n];
            Arrays.fill(queens, -1);
            backtrack(queens, n, 0);
            return solutions;
        }
    
        public void backtrack(int[] queens, int n,int row) {
            if (row == n) {
                List<String> board = generateBoard(queens, n);
                solutions.add(board);
            } else {
                for (int i = 0; i < n; i++) {
                    if (columns.contains(i)) {
                        continue;
                    }
                    int diagonal1 = row - i;
                    if (diagonals1.contains(diagonal1)) {
                        continue;
                    }
                    int diagonal2 = row + i;
                    if (diagonals2.contains(diagonal2)) {
                        continue;
                    }
                    queens[row] = i;
                    columns.add(i);
                    diagonals1.add(diagonal1);
                    diagonals2.add(diagonal2);
                    backtrack(queens, n, row + 1);
                    queens[row] = -1;
                    columns.remove(i);
                    diagonals1.remove(diagonal1);
                    diagonals2.remove(diagonal2);
                }
            }
        }
    
        public List<String> generateBoard(int[] queens, int n) {
            List<String> board = new ArrayList<String>();
            for (int i = 0; i < n; i++) {
                char[] row = new char[n];
                Arrays.fill(row, '.');
                row[queens[i]] = 'Q';
                board.add(new String(row));
            }
            return board;
        }
    }
    

37. 解数独

  • 题目描述

    image.png

  • 题解

    class Solution {
        public void solveSudoku(char[][] board) {
            solveSudokuHelper(board);
        }
    
        private boolean solveSudokuHelper(char[][] board){
            //「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
            // 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
            for (int i = 0; i < 9; i++){ // 遍历行
                for (int j = 0; j < 9; j++){ // 遍历列
                    if (board[i][j] != '.'){ // 跳过原始数字
                        continue;
                    }
                    for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适
                        if (isValidSudoku(i, j, k, board)){
                            board[i][j] = k;
                            if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
                                return true;
                            }
                            board[i][j] = '.';
                        }
                    }
                    // 9个数都试完了,都不行,那么就返回false
                    return false;
                    // 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
                    // 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
                }
            }
            // 遍历完没有返回false,说明找到了合适棋盘位置了
            return true;
        }
    
        /**
         * 判断棋盘是否合法有如下三个维度:
         *     同行是否重复
         *     同列是否重复
         *     9宫格里是否重复
         */
        private boolean isValidSudoku(int row, int col, char val, char[][] board){
            // 同行是否重复
            for (int i = 0; i < 9; i++){
                if (board[row][i] == val){
                    return false;
                }
            }
            // 同列是否重复
            for (int j = 0; j < 9; j++){
                if (board[j][col] == val){
                    return false;
                }
            }
            // 9宫格里是否重复
            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. 递增子序列

  • 题目描述

    image.png

  • 题解

    用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图

    491. 递增子序列1

    去重逻辑:同一父节点下的同层上使用过的元素就不能在使用了

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> findSubsequences(int[] nums) {
            backtracking(nums,0);
            return res;
        }
        public void backtracking(int[] nums,int startIndex){
            if(temp.size()>1){
                res.add(new ArrayList(temp));
              	//注意这里不要return,要取树上的节点
            }
          	//注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希
          	//记录元素 在当前层是否使用
            int[] used = new int[201];
            for(int i=startIndex;i<nums.length;i++){
                if(!temp.isEmpty() && nums[i]<temp.get(temp.size()-1) || used[nums[i]+100]==1) continue;
                temp.add(nums[i]);
              	//记录这个元素在本层用过了,本层后面不能再用了
                used[nums[i]+100]=1;
                backtracking(nums,i+1);
              	//注意这里不要取消已使用元素的状态,当前层一直保持
                temp.remove(temp.size()-1);
            }
        }
    }
    

332. 重新安排行程

  • 题目描述

    image.png

    image.png

  • 题解

    直觉上来看 这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。

    实际上确实是深搜,但这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢。所以我倾向于说本题应该使用回溯法,那么我也用回溯法的思路来讲解本题,其实深搜一般都使用了回溯法的思路

    为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环

    **可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。**如果“航班次数”大于零,说明目的地还可以飞,如果如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作

    相当于说我不删,我就做一个标记!

    本题以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下

    332.重新安排行程1

    class Solution {
        private Deque<String> res;
        private Map<String, Map<String, Integer>> map;
    
        private boolean backTracking(int ticketNum){
            if(res.size() == ticketNum + 1){
                return true;
            }
            String last = res.getLast();
            if(map.containsKey(last)){//防止出现null
                for(Map.Entry<String, Integer> target : map.get(last).entrySet()){
                    int count = target.getValue();
                    if(count > 0){
                        res.add(target.getKey());
                        target.setValue(count - 1);
                        if(backTracking(ticketNum)) return true;
                        res.removeLast();
                        target.setValue(count);
                    }
                }
            }
            return false;
        }
    
        public List<String> findItinerary(List<List<String>> tickets) {
            map = new HashMap<String, Map<String, Integer>>();
            res = new LinkedList<>();
            for(List<String> t : tickets){
                Map<String, Integer> temp;
                if(map.containsKey(t.get(0))){
                    temp = map.get(t.get(0));
                    temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);
                }else{
                    temp = new TreeMap<>();//升序Map
                    temp.put(t.get(1), 1);
                }
                map.put(t.get(0), temp);
    
            }
            res.add("JFK");
            backTracking(tickets.size());
            return new ArrayList<>(res);
        }
    }
    

CodeTop系列

39. 组合总和

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> combinationSum(int[] candidates, int target) {
            Arrays.sort(candidates);
            backtracking(candidates,target,0,0);
            return res;
        }
        public void backtracking(int[] candidates ,int target,int startIndex,int sum){
            if(sum==target){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=startIndex;i<candidates.length;i++){
                if(sum+candidates[i]>target) break;
                temp.add(candidates[i]);
                sum+=candidates[i];
                backtracking(candidates,target,i,sum);
                sum-=temp.get(temp.size()-1);
                temp.remove(temp.size()-1);
            }
        }
    }
    

40. 组合总和 II

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        boolean[] used;
        public List<List<Integer>> combinationSum2(int[] candidates, int target) {
            Arrays.sort(candidates);
            used=new boolean[candidates.length];
            backtracking(candidates,target,0,0);
            return res;
        }
        public void backtracking(int[] candidates,int target,int sum,int startIndex){
            if(target==sum){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=startIndex;i<candidates.length;i++){
                if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;
                if(sum+candidates[i]>target) break;
                temp.add(candidates[i]);
                used[i]=true;
                sum+=candidates[i];
                backtracking(candidates,target,sum,i+1);
                sum-=temp.get(temp.size()-1);
                used[i]=false;
                temp.remove(temp.size()-1);
            }
        }
    }
    

46. 全排列

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        boolean[] used;
        public List<List<Integer>> permute(int[] nums) {
            used=new boolean[nums.length];
            backtracking(nums);
            return res;
        }
    
        public void backtracking(int[] nums){
            if(temp.size()==nums.length){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=0;i<nums.length;i++){
                if(used[i]) continue;
                temp.add(nums[i]);
                used[i]=true;
                backtracking(nums);
                used[i]=false;
                temp.remove(temp.size()-1);
            }
        }
    }
    

47. 全排列 II

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        boolean[] used;
        public List<List<Integer>> permuteUnique(int[] nums) {
            Arrays.sort(nums);
            used=new boolean[nums.length];
            backtracking(nums);
            return res;
        }
        public void backtracking(int[] nums){
            if(temp.size()==nums.length){
                res.add(new ArrayList(temp));
                return;
            }
            for(int i=0;i<nums.length;i++){
                if(i>0 && nums[i]==nums[i-1] && used[i-1]==false) continue;
                if(used[i]) continue;
                temp.add(nums[i]);
                used[i]=true;
                backtracking(nums);
                used[i]=false;
                temp.remove(temp.size()-1);
            }
        }
    }
    

22. 括号生成

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<String> res=new ArrayList<>();
        StringBuilder sb=new StringBuilder();
        public List<String> generateParenthesis(int n) {
            backtracking(0,0,n);
            return res;
        }
        public void backtracking(int left,int right,int n){
            if(left==n && right==n){
                res.add(sb.toString());
                return;
            }
            if(left<n){
                sb.append("(");
                backtracking(left+1,right,n);
                sb.deleteCharAt(sb.length()-1);
            }
            if(right<left){
                sb.append(")");
                backtracking(left,right+1,n);
                sb.deleteCharAt(sb.length()-1);
            }
        }
    }
    

93. 复原 IP 地址

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<String> res=new ArrayList<>();
        public List<String> restoreIpAddresses(String s) {
            backtracking(s,0,0);
            return res;
        }
        public void backtracking(String s,int startIndex,int pointNum){
            if(pointNum==3){
                if(isValid(s,startIndex,s.length()-1)){
                    res.add(s);
                    return;
                }
            }
            for(int i=startIndex;i<s.length();i++){
                if(isValid(s,startIndex,i)){
                    s=s.substring(0,i+1)+"."+s.substring(i+1);
                    pointNum++;
                    backtracking(s,i+2,pointNum);
                    pointNum--;
                    s=s.substring(0,i+1)+s.substring(i+2);
                }else{
                    break;
                }
            }
        }
        public boolean isValid(String str,int start,int end){
            if(start>end) return false;
            if(str.charAt(start)=='0' && start!=end) return false;
            int num=0;
            for(int i=start;i<=end;i++){
                num=num*10+str.charAt(i)-'0';
                if(num>255) return false;
            }
            return true;
        }
    }
    

78. 子集

  • 题目描述

    image.png

  • 题解

    class Solution {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> temp=new ArrayList<>();
        public List<List<Integer>> subsets(int[] nums) {
            backtracking(nums,0);
            return res;
        }
    
        public void backtracking(int[] nums,int starIndex){
            res.add(new ArrayList(temp));
            if(starIndex==nums.length){
                return;
            }
            for(int i=starIndex;i<nums.length;i++){
                temp.add(nums[i]);
                backtracking(nums,i+1);
                temp.remove(temp.size()-1);
            }
        }
    }
    

79. 单词搜索

  • 题目描述

    image.png

  • 题解

    /**
    * 回溯法:相比于DFS,多了一步『撤销修改节点状态』
    */
    class Solution {
        private boolean find;  
        public boolean exist(char[][] board, String word) {
            if (board == null) return false;
            int m = board.length, n = board[0].length;
            boolean[][] visited = new boolean[m][n];
            find = false;
    
            for (int i = 0; i < m; i++){
                for (int j = 0; j < n; j++){
                  	//从左上角开始遍历棋盘每个格子
                    backtracking(i, j, board, word, visited, 0);
                }
            }
            return find;
        }
        /**
        * i,j,board:棋盘格及当前元素的坐标
        * word: 要搜索的目标单词
        * visited:记录当前格子是否已被访问过
        * pos: 记录目标单词的字符索引,只有棋盘格字符和pos指向的字符一致时,
        * 才有机会继续搜索接下来的字符;如果pos已经过了目标单词的尾部了,那么便说明找到目标单词了
        */
        public void backtracking(int i, int j, char[][] board, String word, boolean[][] visited, int pos){
            // 超出边界、已经访问过、已找到目标单词、棋盘格中当前字符已经和目标字符不一致了
            if(i<0 || i>= board.length || j<0 || j >= board[0].length || visited[i][j] || find
               || board[i][j]!=word.charAt(pos))  return;
    
            if(pos == word.length()-1){
                find = true;
                return;
            }
            //修改当前节点状态
            visited[i][j] = true; 
            //遍历子节点
            backtracking(i+1, j, board, word, visited, pos+1); 
            backtracking(i-1, j, board, word, visited, pos+1);
            backtracking(i, j+1, board, word, visited, pos+1);
            backtracking(i, j-1, board, word, visited, pos+1);
            visited[i][j] = false; // 撤销修改
        }
    
    }