代码随想录自刷08:回溯算法

53 阅读12分钟

77. 组合

思路:求的是组合,所以[1,2][2,1]这种算是同一个,不能重复计算。因此需要一个标识idx:来表明当前的横向遍历范围是从哪里开始的!

终止条件: 题目要的是能组成k 个数的所有组合。所以终止条件就是当前组合长度path等于k时,就可以把当前存储的组合path加入到最终集合结果result中。

横向遍历集合:从头开始遍历整个集合,一开始会传入idx的初始值,作为遍历的起头。

纵向遍历集合:因为不能含重复组合,所以要从idx+1作为纵向遍历的起头

class Solution {
    List<List<Integer>> result=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        backTrack(n,k,1);
        return result;
    }
    public void backTrack(int n,int k,int idx){
        //终止条件
        if(path.size()==k){
            //存储符合的结果
            result.add(new ArrayList<>(path));
            return;
        }
        //横向
        for(int i=idx;i<=n;i++){
            path.add(i);
            //纵向
            backTrack(n,k,i+1);
            //回溯
            path.remove(path.size()-1);
        }
    }
}

216. 组合总和 III

思路:跟上题差不多,但要注意这里的k是使用的元素个数,n是元素累加要等于的和。

所以在终止条件判断时,是要用k来判断当前集合path中的个数是否符合要求,再接着判断累加值是否符合n,如果是则加入最终结果集合result中。

class Solution {
    List<List<Integer>> result=new ArrayList<>();
    //也可以跟上题一样用ArrayList,但删除元素时方法不同
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        backTrack(k,n,1,0);
        return result;
    }
    public void backTrack(int k,int n,int idx,int sum){
        if(path.size()==k){
            if(sum==n){
                result.add(new ArrayList<>(path));
            }
            return;
        }
        for(int i=idx;i<=9;i++){
            sum+=i;
            path.add(i);
            backTrack(k,n,i+1,sum);
            sum-=i;
            //LinkedList删除元素
            path.removeLast();
        }
    }
}

17. 电话号码的字母组合

思路:稍微复杂一点,但理清楚怎么进行遍历就好!首先每一个数字都会对应一组字母,例如2对应abc,3对于def,注意0和1没有对应的字母!

假设我按23,能呈现的组合就有:ad,ae,af,bd,be,bf,cd,ce,cf。看出一点感觉了,每次横向遍历要遍历的元素就是:每个数字对应的字母组!所以在遍历前需要先获得当前数字对应的字母组是谁!然后再开始横向遍历。

class Solution {
    List<String> list=new ArrayList<>();
    public List<String> letterCombinations(String digits) {
        if(digits==null || digits.length()==0)
            return list;
        //strs:是数组,记录每个数字对应的字母
        String[] strs={"","","abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        backTrack(digits,0,strs,new StringBuilder());
        return list;
    }
    //idx:遍历digits的元素,获取按到的数字,数字会对应一组字母集合(strs记录了对应关系)
    //sb:用来记录当前字母的组合
    public void backTrack(String digits,int idx,String[] strs,StringBuilder sb){
        //终止条件
        if(idx>=digits.length()){
            list.add(sb.toString());
            return;
        }
        //获取当前要遍历的字母集合(横向遍历)
        String str=strs[digits.charAt(idx)-'0'];
        for(int i=0;i<str.length();i++){
            sb.append(str.charAt(i));
            //纵向遍历
            //idx+1:因为下次横向遍历时,数字要往后一个才能获得对应字母组!
            backTrack(digits,idx+1,strs,sb);
            sb.deleteCharAt(sb.length()-1);
        }
    }
}

39. 组合总和

思路:这题解题思路差不多,但是要注意!元素可以重复获取! 所以纵向遍历时要注意一下方法中的参数不用加一哦!

class Solution {
    List<List<Integer>> list=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates.length==0 || candidates==null)
            return list;
        backTrack(candidates,target,0);
        return list;
    }
    public void backTrack(int[] candidates,int target,int idx){
        //终止条件
        if(target<=0){
            if(target==0){
                list.add(new ArrayList<>(path));
            }
            return;
        }
        for(int i=idx;i<candidates.length;i++){
            path.add(candidates[i]);
            //可以重复取元素,所以i不用+1
            backTrack(candidates,target-candidates[i],i);
            path.removeLast();
        }
    }
}

40. 组合总和 II(注意)

思路:这题比前面更难一点,题目重点:

  • 有重复的元素
  • 每个元素只能使用一次
  • 并且结果集合不能包含重复的组合!
  1. 前面两个要求,可以用一个布尔值数组来记录,每个元素是否被用到!如果正在使用元素需要标记为true,没有使用或者已经释放了元素要标记为false。

  2. 如何避免重复组合? 答:同一树层使用过的元素不能重复选取,这样才能避免结果组合重复!

    例如同一树层中,有两个1,第一个1之前使用过了,所以当前树层不能再用1,于是第二个1需要跳过。(注意元素在同一个组合内(树枝)是可以重复的,但是不能有两个组合具备完全相同元素,所以是树层去重复)

  3. 如何判断树层中,当前元素是否需要跳过?

    下面代码表示:前后两个元素相同,并且之前使用过这个元素但是已经把它释放了(所以标记为false),说明同一树层中该元素曾经被使用过!,避免结果组合重复,要跳过这个元素(放弃它这一树枝)

if(i>0 && candidates[i-1]==candidates[i] && used[i-1]==false){
    continue;
}

注意!因为有重复的元素,所以集合要先进行排序!

class Solution {
    List<List<Integer>> list=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if(candidates==null || candidates.length==0)
            return list;
        Arrays.sort(candidates);
        //used:确保元素是否使用过,如果是则true,否则为false
        boolean[] used=new boolean[candidates.length];
        backTrack(candidates,target,0,used);
        return list;
    }
    public void backTrack(int[] candidates,int target,int start,boolean[] used){
        if(target<=0){
            if(target==0){
                list.add(new ArrayList<>(path));
            }
            return;
        }
        for(int i=start;i<candidates.length;i++){
            //表示之前的树枝用过这个元素了并且把它释放了所以为false,说明同一树层中该元素曾经被使用过!,避免结果组合重复,要跳过这个元素(放弃它这一树枝)
            if(i>0 && candidates[i-1]==candidates[i] && used[i-1]==false){
                continue;
            }
            else{
                //使用该元素,记录一下
                used[i]=true;
                path.add(candidates[i]);
                backTrack(candidates,target-candidates[i],i+1,used);
                //释放该元素,记录一下
                used[i]=false;
                path.removeLast();
            }
        }
    }
}

131. 分割回文串

思路:把一树枝(纵向)都切割好,再放入到结果集合中。所以终止条件是:所有元素都遍历完。

要有一个方法来判断是否为回文子串。

class Solution {
    List<List<String>> list=new ArrayList<>();
    LinkedList<String> path=new LinkedList<>();
    public List<List<String>> partition(String s) {
        if(s==null || s.length()==0)
            return list;
        backTrack(s,0);
        return list;
    }
    public void backTrack(String s,int idx){
        if(idx==s.length()){
            list.add(new ArrayList<>(path));
            return;
        }
        for(int i=idx;i<s.length();i++){
            //是回文子串才进行递归
            if(compare(s,idx,i)){
                path.add(s.substring(idx,i+1));
                backTrack(s,i+1);
                path.removeLast();
            }
        }
    }
    public boolean compare(String s,int start,int end){
        if(start>end)
            return false;
        while(start<=end){
            if(s.charAt(start)==s.charAt(end)){
                start++;
                end--;
            }
            else
                return false;
        }
        return true;
    }
}

93. 复原 IP 地址

思路:

  1. 首先需要一个方法判断选出的值是否符合要求(0-255之间)。
  2. 思考回溯方法的参数有哪些,通常都有一个起始下标变量idx,再来一个变量point记录加入的点数量,方便作为终止条件的判断!
  3. 符合终止条件后,里面还需要再判断一次最后的值是否符合要求!因为它在之前的遍历过程中是没有被判断到的(第三个点加上去的时候,判断的是第三个值符合要求)所以最后的值需要在终止条件中进行判断,符合才可以加入结果集合中
class Solution {
    List<String> list=new ArrayList<>();
    public List<String> restoreIpAddresses(String s) {  
        if(s.length()>12)
            return list;
        backTrack(s,0,0);
        return list;
    }
    public void backTrack(String s,int idx,int point){
        //需要一个变量:记录.的数量
        if(point==3){
            //判断最后一区段是否符合要求(0-255)
            if(isValid(s,idx,s.length()-1)){
                list.add(s);
            }
            return;
        }
        for(int i=idx;i<s.length();i++){
            if(isValid(s,idx,i)){
                s=s.substring(0,i+1)+"."+s.substring(i+1);
                //注意参数是i+2,要跳过上面刚加入的.(它下标是i+1)
                backTrack(s,i+2,point+1);
                //跳过上面刚加入的.
                s=s.substring(0,i+1)+s.substring(i+2);
            }
        }
    }
    public boolean isValid(String s,int start,int end){
        if(start>end)   
            return false;
        //不能含有前导0
        if(s.charAt(start)=='0' && start!=end)
            return false;
        int sum=0;
        for(int i=start;i<=end;i++){
            char ch=s.charAt(i);
            if(ch>'9' || ch<'0')
                return false;
            sum=sum*10+ch-'0';
            if(sum>255)
                return false;
        }
        return true;
    }
}

78. 子集

思路:返回该数组所有可能的子集,所以跟之前不同的是:每一个树节点都要加入到结果集合中,以前都是收集树的叶子节点(不是所有节点)。

如何收集每一个树节点?答:在循环外面,其他步骤没变

class Solution {
    List<List<Integer>> list=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
        if(nums.length==0 || nums==null)
            return list;
        backTrack(nums,0);
        return list;
    }
    public void backTrack(int[] nums,int start){
        //加入结果集合中
        list.add(new ArrayList<>(path));
        for(int i=start;i<nums.length;i++){
            path.add(nums[i]);
            backTrack(nums,i+1);
            path.removeLast();
        }
    }
}

90. 子集 II(注意)

思路:这题和上一题目的区别就在于有重复元素。题目要求结果必须是去重,看到去重,就要想到用布尔值数组来记录元素的使用!这里实现的是树层去重

class Solution {
    List<List<Integer>> list=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        //数组要先排序!
        Arrays.sort(nums);
        boolean[] used=new boolean[nums.length];
        backTrack(nums,0,used);
        return list;
    }
    public void backTrack(int[] nums,int start,boolean[] used){
        list.add(new ArrayList<>(path));
        for(int i=start;i<nums.length;i++){
            if(i>0 && nums[i-1]==nums[i] && used[i-1]==false)
                continue;
            else{
                used[i]=true;
                path.add(nums[i]);
                backTrack(nums,i+1,used);
                path.removeLast();
                used[i]=false;
            }
        }
    }
}

491. 递增子序列(注意)

思路:这题有难度,去重方法跟之前不一样。首先题目要我们返回排序的子集,并且不能有重复结果,数组中还有可能有重复的元素。

  1. 根据要求,不能先给数组排序,这样以前用的去重方法就不能使用。那要如何知道哪些元素已经使用过了要跳过呢?

    答:用set来存储:同一父节点下,本层使用过的元素! 注意这个set要在哪里创建很重要,他要记录同一父节点下,本层使用过的元素,因此应该是在进入循环前就要创建。

  2. 数组中有重复的元素,要进行处理吗?

    答:不需要处理,因为题目说:相等的数字应该被视为递增的一种情况。

class Solution {
    List<List<Integer>> list=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        if(nums.length<2)
            return list;
        backTrack(nums,0);
        return list;
    }
    public void backTrack(int[] nums,int start){
        if(path.size()>=2){
            list.add(new ArrayList<>(path));
            return;
        }
        //同一父节点下的同层元素不能重复使用!
        Set<Integer> set=new HashSet<>();
        for(int i=start;i<nums.length;i++){
            if(!path.isEmpty() && path.getLast()>nums[i] || set.contains(nums[i]))
                continue;
            else{
                set.add(nums[i]);
                path.add(nums[i]);
                backTrack(nums,i+1);
                path.removeLast();
            }
        }
    }
}

46. 全排列

思路:进入排列问题,那么像:[1,2,3][2,1,3]这种就算不同的结果是符合要求的。所以不需要变量标识循环的起始下标了(舍弃start(idx)变量),每次循环都是从第一个元素开始!

那要如何跳过path中已有的元素呢?

答:用布尔值数组来记录!此时判断条件:如果为true表示该元素在树枝上使用过了(path中已有),需要跳过!

class Solution {
    List<List<Integer>> list=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        boolean[] used=new boolean[nums.length];
        if(nums.length==0 || nums==null)
            return list;
        backTrack(nums,used);
        return list;
    }
    public void backTrack(int[] nums,boolean[] used){
        if(path.size()==nums.length){
            list.add(new ArrayList<>(path));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(used[i])
                continue;
            used[i]=true;
            path.add(nums[i]);
            backTrack(nums,used);
            used[i]=false;
            path.removeLast();
            
        }
    }
}

47. 全排列 II

思路:跟上一题目的区别在于,有重复的元素。所以除了要思考树枝,还要思考树层。

  1. 首先树枝要如何跳过已经使用的元素?

    答:当userd[i]=true时,表示path中已经使用该元素,要跳过

  2. 树层如何跳过重复的元素?

    答:当前后元素相同,并且userd[i-1]=false,表示前一个元素已经使用过并且释放掉,所以要跳过当前元素

class Solution {
    List<List<Integer>> list=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        if(nums==null || nums.length==0)
            return list;
        Arrays.sort(nums);
        boolean[] used=new boolean[nums.length];
        backTrack(nums,used);
        return list;
    }
    public void backTrack(int[] nums,boolean[] used){
        if(path.size()==nums.length){
            list.add(new ArrayList<>(path));
            return;
        }
        for(int i=0;i<nums.length;i++){
            //过滤树层
            if(i>0 && nums[i-1]==nums[i] && used[i-1]==false)
                continue;
            //过滤树枝:used[i]=true表示树枝上已经使用该元素,要跳过
            if(used[i]==false){
                used[i]=true;
                path.add(nums[i]);
                backTrack(nums,used);
                used[i]=false;
                path.removeLast();
            }
        }
    }
}

51. N 皇后

思路:

  • 不能同行
  • 不能同列
  • 不能同斜线(45度和135度)

需要一个方法判断,当前坐标放上棋子,是否符合以上要求(只需要检查当前坐标之前的即可,因为后面都没放棋子所以肯定符合要求)

再来一个方法将当前结果数组转为list集合!

  class Solution {
    List<List<String>> list=new ArrayList<>();
    public List<List<String>> solveNQueens(int n) {
        char[][] board=new char[n][n];
        for(char[] ch : board){
            Arrays.fill(ch,'.');
        }
        backTrack(n,0,board);
        return list;
    }
    public void backTrack(int n,int row,char[][] board){
        if(row==n){
            list.add(ArraysToList(board));
            return;
        }
        for(int col=0;col<n;col++){
            if(isValid(n,board,row,col)){
                board[row][col]='Q';
                backTrack(n,row+1,board);
                board[row][col]='.';
            }
        }
    }
    //转换集合
    public List<String> ArraysToList(char[][] board){
        List<String> list=new ArrayList<>();
        for(char[] ch : board){
            list.add(new String(ch));
        }
        return list;
    }
    //判断是否符合要求
    public boolean isValid(int n,char[][] board,int row,int col){
        //检查行
        for(int i=0;i<row;i++){
            if(board[i][col]=='Q')
                return false;
        }
        //检查列
        for(int i=0;i<col;i++){
            if(board[row][i]=='Q')
                return false;
        }
        // 检查45度对角线
        for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--){
            if(board[i][j]=='Q')
                return false;
        }
        // 检查135度对角线
        for(int i=row-1,j=col+1;i>=0 && j<n;i--,j++){
            if(board[i][j]=='Q')
                return false;
        }
        return true;
    }
}

37. 解数独

思路:跟皇后一样,要判断当前位置放该数字是否符合要求,注意要判断两个九宫格!一个大九宫格和一个小九宫格。

可能存在:当前位置无法放入任何数字,那就是无解,返回false即可。

class Solution {
    public void solveSudoku(char[][] board) {
        backTrack(board);
    }
    public boolean backTrack(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 ch='1';ch<='9';ch++){
                        if(isValid(board,i,j,ch)){
                            board[i][j]=ch;
                            if(backTrack(board))
                                return true;  //找到结果返回
                            //记得回溯
                            board[i][j]='.';
                        }
                    }
                    //9个数字都填不进去肯定无解
                    return false;
                }
            }
        }
        return true;
    }
    public boolean isValid(char[][] board,int row,int col,char k){
        //检查大九宫格的范围
        //检查行
        for(int i=0;i<board.length;i++){
            if(board[i][col]==k)
                return false;
        }
        //检查列
        for(int i=0;i<board[0].length;i++){
            if(board[row][i]==k)
                return false;
        }
        //检查小九宫格(要先获取小九宫格行列的起始位置)
        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]==k)
                    return false;
            }
        }
        return true;
    }
}