LeetCode_回溯算法刷题笔记(Java)

1,172 阅读8分钟

在刷LeetCode的时候,看到代码随想录上关于回溯算法的总结很到位,并且提供了总结的pdf。所以就下载了该pdf,对照着顺序刷了下题。原作者是用C++刷的,我主要参考了思想,然后用Java写的。我就不写思路,感兴趣的Java开发同学,可以到该公众号上下载对应pdf,然后对照下我的代码看看。在此说明下,本文只是个刷题记录,没有详细的思路讲解。

image-20210218103908678

模板:

void backtracking(参数){
  if(结束条件){
    记录结果;
    return;
  }
  
  for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
    处理节点;
    backtracking(参数);
    回溯,撤销之前记录的数据;
  }
  
}

for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历

组合

组合是无序的,所以不能重复。比如{1,2}、{2,1}在组合中会被认为是相同的,而在排序中却是不同的。

所以在组合的过程中需要去除重复部分,即 剪枝

77. 组合

难度中等493

给定两个整数 nk,返回 1 ... n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

解题

这里使用了个startIndex,来剪枝。

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result=new ArrayList();
        if(n==0)return result;
        backtracking(n,0,k,1,new ArrayList<Integer>(k),result);
        return result;
    }

    private void backtracking(int n,int deep,int k,int startIndex,List<Integer> tempList,List<List<Integer>> result){
        if(deep==k){
            result.add(new ArrayList(tempList));
            return ;
        }
        for(int i =startIndex;i<=n;i++){
            tempList.add(i);
          	//startIndex=i+1;剪枝
          	//递归
            backtracking(n,deep+1,k,i+1,tempList,result);
          	//回溯
            tempList.remove(tempList.size()-1);
        }
    }
}

image-20210217123726827

优化

如果最终组成的个数是达不到K个,这部分也可以剪去。

如何剪去?即:当前个数+剩余可用元素>K才可继续组合。用上面的代码体现为:

tempList.size()+(n-i+1)>k

可转换成

n-k+tempList.size()+1>i 

所以原先代码的for循环优化为如下:

 for(int i =startIndex;i<=n-(k-tempList.size())+1;i++){
   ...
 }

优化后代码:

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result=new ArrayList();
        if(n==0)return result;
        backtracking(n,0,k,1,new ArrayList<Integer>(),result);
        return result;
    }

    private void backtracking(int n,int deep,int k,int startIndex,List<Integer> tempList,List<List<Integer>> result){
        if(deep==k){
            result.add(new ArrayList(tempList));
            return ;
        }
        
        for(int i =startIndex;i<=n-(k-tempList.size())+1;i++){
            tempList.add(i);
            backtracking(n,deep+1,k,i+1,tempList,result);
            tempList.remove(tempList.size()-1);
        }
    }
}

image-20210217123726827

216. 组合总和 III

难度中等261

找出所有相加之和为 nk* 个数的组合**。***组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

解法

class Solution {
    final int MAX_INT=9;
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> result=new ArrayList();
        backtracking(0,k,0,n,1,new ArrayList<Integer>(),result);
        return result;
    }

    private void backtracking(int sum,int k,int deep,int n,int startIndex,List<Integer> temp,List<List<Integer>> result){
        if(deep==k){
          	//个数为k,总和是n才符合
            if(sum==n){
            result.add(new ArrayList(temp));
            }
            return;
        }

        for(int i =startIndex;i<=MAX_INT;i++){
          	//如果总和已经超过了n,后面就没必要了-->剪枝
            if(sum+i>n)continue;
            sum=sum+i;
            temp.add(i);
          //startIndex=i+1;避免出现重复元素
            backtracking(sum,k,deep+1,n,i+1,temp,result);
          	//回溯
            temp.remove(temp.size()-1);
            sum=sum-i;
        }
    }
}

image-20210217130326233

17. 电话号码的字母组合

难度中等1128

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

img

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。

解法

class Solution {
     private char[][] lettersArray=
    {
        {'a','b','c'},{'d','e','f'},
        {'g','h','i'},{'j','k','l'},{'m','n','o'},
        {'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}
    };
    public List<String> letterCombinations(String digits) {
        List<String> result=new ArrayList();
        if(digits.length()==0) return result;
        char[] chars=digits.toCharArray();
        backtracking(chars,0,new char[chars.length],result);
        return result;
    }

    private void backtracking(char[] digits,int deep,char[] temp,List<String> result){
        if(deep==temp.length){
            result.add(new String(temp));
            return;
        }
      
        for(char item :lettersArray[digits[deep]-'2']){
            temp[deep]=item;
            backtracking(digits,deep+1,temp,result);
            //不用回溯,会被覆盖
        }
    }
}

image-20210217132546907

39. 组合总和

难度中等1158

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。

示例 1:

输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]

示例 2:

输入:candidates = [2,3,5], target = 8,
所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

提示:

  • 1 <= candidates.length <= 30
  • 1 <= candidates[i] <= 200
  • candidate 中的每个元素都是独一无二的。
  • 1 <= target <= 500

解法

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> result=new ArrayList();

    backtracking(0,0,candidates,target,new ArrayList<Integer>(),result);
        return result;
    }

    private void backtracking(int sum,int startIndex,int[] candidates,int target,List<Integer> temp,List<List<Integer>> result){
        if(sum>target){
            return;
        }
        if(sum==target){
            result.add(new ArrayList(temp));
            return;
        }

        for(int i=startIndex;i<candidates.length;i++){
            if(sum+candidates[i]>target) continue;
            sum=sum+candidates[i];
            temp.add(candidates[i]);
            backtracking(sum,i,candidates,target,temp,result);
            sum=sum-candidates[i];
            temp.remove(temp.size()-1);
        }
    }
}

image-20210217142732048

40. 组合总和 II

难度中等491

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

  • 所有数字(包括目标数)都是正整数。
  • 解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
  [1,2,2],
  [5]
]

解法

本题与38题的区别:

	-	 **提供的数组中的元素可能会重复**
	-	 **提供的数组中的元素只能使用一次**

所以 startIndex需要+1,其次是 去重

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> result =new ArrayList();
        //排序,方便后面去重
        Arrays.sort(candidates);
        backtracking(0,0,target,candidates,new ArrayList<Integer>(),result);
        return result;
    }

    private void backtracking(int sum,int startIndex,int target,int[] candidates,List<Integer> temp,List<List<Integer>> result){
        if(sum==target){
            result.add(new ArrayList(temp));
            return;
        }

        for(int i =startIndex;i<candidates.length;i++){
            if(sum+candidates[i]>target) continue;
            //元素去重
            if(i!=startIndex&&candidates[i]==candidates[i-1]) continue;
            temp.add(candidates[i]);
            sum=sum+candidates[i];
            //startIndex=i+1,不用之前的元素
            backtracking(sum,i+1,target,candidates,temp,result);
            temp.remove(temp.size()-1);
            sum=sum-candidates[i];
        }
    }
}

分割

131. 分割回文串

难度中等485

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例:

输入: "aab"
输出:
[  ["aa","b"],
  ["a","a","b"]
]

解法

class Solution {
    public List<List<String>> partition(String s) {
        List<List<String>> result= new ArrayList();
        backtracking(s,0,new ArrayList<String>(),result);
        return result;
    }

    private void backtracking(String s,int startIndex,List<String> path,List<List<String>> result){
        int length=s.length();
        if(startIndex>=length){
            result.add(new ArrayList<String>(path));
            return;
        }
        for(int i=startIndex;i<length;i++){
            if(!isHuiwen(startIndex,i,s)) continue;
            path.add(s.substring(startIndex,i+1));
            backtracking(s,i+1,path,result);
            path.remove(path.size()-1);
        }
    }

    private boolean isHuiwen(int startIndex,int endIndex,String s){
        for(int i=startIndex, j=endIndex;i<j;i++,j--){
            if(s.charAt(i)!=s.charAt(j))return false;
        }
        return true;
    }
}

image-20210217155345393

132. 分割回文串 II 也可以通过上述类似方法解决,不过会超时。参考动态规划解决。

93. 复原IP地址

难度中等501

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效的 IP 地址。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "1111"
输出:["1.1.1.1"]

示例 4:

输入:s = "010010"
输出:["0.10.0.10","0.100.1.0"]

示例 5:

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示:

  • 0 <= s.length <= 3000
  • s 仅由数字组成

解法

class Solution {
    private static final int SEG_COUNT=4;
    public List<String> restoreIpAddresses(String s) {
        List<String> result= new ArrayList<String>();
        int length=s.length();
        if(length<4||length>12) return result;
        backtracking(s,length,0,0,new int[SEG_COUNT],result);
        return result;
    }

    private void backtracking(String s,int length,int segId,int startIndex,int[] temp,List<String> result){
         if(segId==SEG_COUNT){
                if(startIndex==length){
                StringBuffer sb=new StringBuffer();
                for(int i=0;i<SEG_COUNT;i++){
                    sb.append(temp[i]);
                    if(i!=SEG_COUNT-1){
                        sb.append('.');
                    }
                }
                result.add(sb.toString());
                
            }
            return;
        }
        if(startIndex==length){
            return;
        }
        if(s.charAt(startIndex)=='0'){
            temp[segId]=0;
            backtracking(s,length,segId+1,startIndex+1,temp,result);
        }else{
            int ip=0;
            for(int i=startIndex;i<length;i++){
                ip=ip*10+(s.charAt(i)-'0');
                if(ip>0xFF||ip<0)break;
                temp[segId]=ip;
                backtracking(s,length,segId+1,i+1,temp,result);
            }
        }

    }
}

image-20210217194749290

子集

78. 子集

难度中等995

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

解法

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result =new ArrayList();
        backtracking(nums,0,new ArrayList<Integer>(),result);
        return result;
    }

    private void backtracking(int[] nums,int startIndex,List<Integer> path,List<List<Integer>> result){
        result.add(new ArrayList(path));
        if(startIndex==nums.length)return;
			
        for(int i=startIndex;i<nums.length;i++){
            path.add(nums[i]);
          	//startIndex=i+1; 去重
            backtracking(nums,i+1,path,result);
            path.remove(path.size()-1);
        }
    }
}

90. 子集 II

难度中等383

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

**说明:**解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

解法

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> result= new ArrayList();
        //排序
        Arrays.sort(nums);
        backtracking(nums,0,new ArrayList<Integer>(),result);
        return result;

    }

    //竖向不去重,横向去重
    private void backtracking(int[] nums,int startIndex,List<Integer> path,List<List<Integer>> result){
        result.add(new ArrayList(path));
        if(startIndex==nums.length) return;

        for(int i =startIndex;i<nums.length;i++){
            //nums得是有序的才行
            if(i!=startIndex&&nums[i]==nums[i-1])continue;
            path.add(nums[i]);
            backtracking(nums,i+1,path,result);
            path.remove(path.size()-1);
        }
    }
}

image-20210217201450614

491. 递增子序列

难度中等

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

示例:

输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

说明:

  1. 给定数组的长度不会超过15。
  2. 数组中的整数范围是 [-100,100]。
  3. 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

解法

需要注意的点,是要 递增的,所以不能使用90. 子集 II的排序后去重的方法了。

class Solution {
    public List<List<Integer>> findSubsequences(int[] nums) {
        List<List<Integer>> result=new ArrayList();
        backtracking(nums,0,new ArrayList<Integer>(),result);
        return result;

    }

    private void backtracking(int[] nums,int startIndex,List<Integer> path,List<List<Integer>> result){
      //递增子序列的长度至少是2
        if(path.size()>1){
            result.add(new ArrayList(path));
        }
        
      	//存储当前层已经使用过的数据了,为了去重
        List<Integer> used=new ArrayList();
        for(int i =startIndex;i<nums.length;i++){
            if(
              	//保持递增,第一次写成了nums[i]<nums[i-1],是不对的
                ( !path.isEmpty()
                    && nums[i]<path.get(path.size()-1))
            ||
              	//当前层去重
                used.contains(nums[i])
              )continue;
            used.add(nums[i]);
            path.add(nums[i]);
          	//startIndex=i+1,竖直方向使用过的元素不再使用
            backtracking(nums,i+1,path,result);
          	//回溯,
            path.remove(path.size()-1);
        }
    }
}

image-20210217213243228

有个命名相似的题目:300. 最长递增子序列,不过这道题使用回溯法来求肯定跟 132. 分割回文串 II类似,会超时。这类求最长什么的用动态规划更合适。

排列

46. 全排列

难度中等1135

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

解法

这道题在竖直方向上需要排重

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result =new ArrayList();
        backtracking(nums,0,new int[nums.length],result);
        return result;
    }
    private List<Integer> used =new ArrayList();
    private void backtracking(int[] nums,int deep,int[] path,List<List<Integer>> result){
        if(deep==nums.length){
            ArrayList<Integer> temp=new ArrayList();
            for(int item:path){
                temp.add(item);
            }
            result.add(new ArrayList(temp));
            return ;
        }

        for(int i=0;i<nums.length;i++){
            if(used.contains(nums[i])) continue;
            used.add(nums[i]);
            path[deep]=nums[i];
           backtracking(nums,deep+1,path,result);
            used.remove(used.size()-1);
        }
    }
}

image-20210217215936511

优化

上述是正常写法,但是效率有点低,下面写法不容易被想到。但是效率高些

class Solution {
    private List<List<Integer>> result;
    private int[] nums;
    public List<List<Integer>> permute(int[] nums) {
        if(nums==null)return null;
        result =new ArrayList();
        if(nums.length==0)return result;
        this.nums=nums;
        dfs(0);
        return result;
    }
    private void dfs(int dp){
        if(dp==nums.length){
            List<Integer> tempList=new ArrayList();
            for(int i:nums){
                tempList.add(i);
            }
            result.add(tempList);
            return;
        }
        for(int i=dp;i<nums.length;i++){
            swap(dp,i);
            dfs(dp+1);
            swap(i,dp);
        }
    }

    private void swap(int i,int j){
        int temp =nums[i];
        nums[i]=nums[j];
        nums[j]=temp;
    }
}

image-20210217220124845

47. 全排列 II

难度中等592

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

解法

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> result= new ArrayList();
        used=new boolean[nums.length];
        Arrays.sort(nums);
        backtracing(nums,0,new ArrayList<Integer>(),result);
        return result;
    }
    private boolean[] used;
    private void backtracing(int[] nums,int deep,List<Integer> path,List<List<Integer>> result){
        if(deep==nums.length){
            result.add(new ArrayList(path));
            return;
        }
        for(int i=0;i<nums.length;i++){
          	//nums[i]与nums[i-1]比较时,必须确保nums[i-1]已经用过了.
            if(i>0&&used[i-1]==false&&nums[i]==nums[i-1])continue;
           if(used[i])continue;
            used[i]=true;
            path.add(nums[i]);
            backtracing(nums,deep+1,path,result);
            path.remove(path.size()-1);
            used[i]=false;
        }
    }
   
  
}

image-20210217230638498

棋盘

51. N 皇后

难度困难749

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

示例 1:

img

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:[["Q"]]
class Solution {
    //存储放皇后的位置
    int[] queens;
    char[] strings;

    public List<List<String>> solveNQueens(int n) {
        List<List<String>> result=new ArrayList();
        if(n<1) return result;
        queens=new int[n];
        strings=new char[n];
        StringBuffer sb=new StringBuffer();
        for(int i =0;i<n;i++){
            strings[i]='.';
        }
        placeQueens(0,result);
        return result;
    }

    private void placeQueens(int row,List<List<String>> result){
        if(row==queens.length){
            List<String> temp=new ArrayList();
            //找到成功结果了
            for(int item:queens){
                strings[item]='Q';
                temp.add(new String(strings));
                strings[item]='.';
            }
            result.add(temp);
            return;
        }
        for(int col=0;col<queens.length;col++){
            if(isVaild(row,col)){
                queens[row]=col;
                placeQueens(row+1,result);
                queens[row]=0;
            }
        }
    }

    private boolean isVaild(int row,int col){
        for(int i=0;i<row;i++){
            if(queens[i]==col)return false;
            if(row-i==Math.abs(queens[i]-col))return false;
        }
        return true;
    }
}

相似:面试题 08.12. 八皇后

52. N皇后 II

难度困难234

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回 n 皇后问题 不同的解决方案的数量。

示例 1:

img

输入:n = 4
输出:2
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:1

提示:

  • 1 <= n <= 9
  • 皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。
class Solution {
    int ways=0;
    public int totalNQueens(int n) {
        if(n<1)return ways;
        int[] cols=new int[n];
        placeQueens(0,cols);
        return ways;
    }
    private void placeQueens(int row,int[] cols){
        if(row==cols.length){
            ways++;
            return;
        }
        for(int i=0;i<cols.length;i++){
            if(isVaild(row,i,cols)){
                cols[row]=i;
                placeQueens(row+1,cols);
                cols[row]=0;
            }
        }
    }
    private boolean isVaild(int row,int col,int[] cols){
        for(int i=0;i<row;i++){
            if(cols[i]==col)return false;
            if(row-i==Math.abs(cols[i]-col)) return false;
        }
        return true;
    }
}