回溯算法

组合问题

组合总数

在这里插入图片描述

  1. 已经选择的元素个数:path.size();
  2. 所需需要的元素个数为: k - path.size();
  3. 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
  4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        backTracking(n,k,1);
        return res;
    }
    public void backTracking(int n,int k,int startIndex){
        if(path.size()==k){
            res.add(new LinkedList<>(path));
            return;
        }
        for(int i=startIndex;i<=(n-(k-path.size())+1);i++){
            path.add(i);
            //递归
            backTracking(n,k,i+1);
            //回溯
            path.removeLast();
        }
    }
}

限制选取个数的组合总和

在这里插入图片描述

递归时相加记录当前和,回溯时相减还原当前和

class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    static int currentSum;
    public List<List<Integer>> combinationSum3(int k, int targetSum) {
        bacKTracking(targetSum,k,currentSum,1);
        return res;
    }
    public void bacKTracking(int targetSum,int k,int currentSum,int startIndex){
        //剪枝,当前和超过了,直接返回
        if(currentSum>targetSum){
            return;
        }
        if(path.size()==k){
            if(currentSum==targetSum){
                res.add(new LinkedList<>(path));
            }
        }
        //遍历宽度剪枝        
        for(int i=startIndex;i<=9-(k-path.size())+1;i++){
            currentSum+=i;
            path.add(i);
            bacKTracking(targetSum,k,currentSum,i+1);
            currentSum-=i;
            path.removeLast();
        }
    }
}

可重复选取的组合总和

在这里插入图片描述

1. 递归终点是当前和大于等于目标和
2. 因为没有数量限制,因此回溯时的开始起点仍然从i开始
3. 剪枝:先排序,当前和+下一个的总和>目标和就break

class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    private int currentSum;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates==null|candidates.length==0){
            return res;
        }
        Arrays.sort(candidates);
        backTracking(candidates,target,0,0);
        return res;
    }
    public void backTracking(int[]candidates,int target,int currentSum,int startIndex){
        if(currentSum>target){
            return;
        }
        if(currentSum==target){
            res.add(new LinkedList(path));
            return;
        }
        for(int i=startIndex;i<candidates.length;i++){
            if(currentSum+candidates[i]>target){
                break;
            }
            currentSum+=candidates[i];
            path.add(candidates[i]);
            //因为数量无限制,因此回溯时仍然从i开始
            backTracking(candidates,target,currentSum,i);
            currentSum-=candidates[i];
            path.removeLast();
        }
    }
}
 

需要去重的组合总和

在这里插入图片描述

去重:用一个used数组来记录已使用过的数,如果candidates[i]==candidates[i-1]&&used[i-1]==false,则跳过
在这里插入图片描述

class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    static boolean[] used;
    static int currentSum;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        used=new boolean[candidates.length+1];
        if(candidates==null||candidates.length==0){
            return res;
        }
        Arrays.fill(used,false);
        Arrays.sort(candidates);
        backTracking(candidates,target,0,0,used);
        return res;
    }

    public void backTracking(int[]candidates,int target,int currentSum,int startIndex,boolean[]used){
        if(currentSum==target){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=startIndex;i<candidates.length;i++){
            if(currentSum+candidates[i]>target){
                break;
            }
            if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false){
                continue;
            }
            currentSum+=candidates[i];
            used[i]=true;
            path.add(candidates[i]);
            backTracking(candidates,target,currentSum,i+1,used);
            currentSum-=candidates[i];
            used[i]=false;
            path.removeLast();
        }
    }
}

电话号码的字母组合

在这里插入图片描述

1. 首先通过一个map来表示数字与字母的映射关系
2. 用一个index表示当前遍历到了digits的第几位了
3. 将digits每一位取出并转化为数字,再得到其对应的字符集
4. 遍历字符集,递归回溯
5.
在涉及字符串的拼接操作时,StringBuilder的效率更高
6.
映射得到的字符集只是一个临时变量,不能声明为static!

class Solution {
    List<String>res=new ArrayList<>(); //最终结果集
    static StringBuilder temp=new StringBuilder(); //拼接单条结果
    static String []letterMap={
        "","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
    //   0  1   2    3      4     5     6      7     8      9     
    };
    public List<String> letterCombinations(String digits) {
        if(digits.length()==0||digits==null){
            return res;
        }
        backTracking(digits,letterMap,0);
        return res;
    }
    /*
        digits:电话号码
        letterMap:数字与字母的映射关系
        index:遍历到号码的第几位数字
    */
    public void backTracking(String digits,String[] letterMap,int index){
        //递归结束条件
        if(index==digits.length()){
            res.add(temp.toString());
            return;
        }
        //将digits中的其中一个数取出,并得到该数对应的字符集
        //不能声名为全局变量
        String str=letterMap[digits.charAt(index)-'0'];
        for(int i=0;i<str.length();i++){
            temp.append(str.charAt(i));
            backTracking(digits,letterMap,index+1);
            temp.deleteCharAt(temp.length()-1);
        }
    }
}

分割问题

分割回文串

在这里插入图片描述
在这里插入图片描述
1. 判断片段是否是回文子串,如果是则截取
2. 否则就跳过,回溯下一层

class Solution {
    List<List<String>>res=new ArrayList<>();
    LinkedList<String>path=new LinkedList<>();
    //双指针判断回文字符串
    public boolean isParlindrom(String s,int start,int end){
        for(int i=start,j=end;i<j;i++,j--){
            if(s.charAt(i)!=s.charAt(j)){
                return false;
            }
        }
        return true;
    }
    public void backTracking(String s,int startIndex){
        if(startIndex>=s.length()){
            res.add(new ArrayList<>(path));
            return;
        }
        //注意循环开始位置是startIndex
        for(int i=startIndex;i<s.length();i++){
            //如果[startIndex,i]是回文字符串,则截取[startIndex,i]
            if(isParlindrom(s,startIndex,i)){
                String str=s.substring(startIndex,i+1);
                path.add(str);
            }else{
                continue;
            }
            backTracking(s,i+1);
            path.removeLast();
        }
    }
    public List<List<String>> partition(String s) {
        backTracking(s,0);
        return res;
    }
}

复原IP地址

在这里插入图片描述
1.写一个判断一个区间是否有效的方法
2.如果有效,则通过拼接插入’.',并记录其数量
3.递归终点:如果分隔符数量为3,且该地址有效,则加入答案

class Solution {
    List<String>res=new ArrayList<>();
    public void backTracking(String s,int startIndex,int pointNum){
        //如果分隔符数量为3,且地址有效,则加入答案
        if(pointNum==3){
            if(isValid(s,startIndex,s.length()-1)){
                res.add(s);
                return;
            }
        }
        for(int i=startIndex;i<s.length();i++){
            //如果[startIndex,i]是有效地址,则添加分隔符并回溯
            if(isValid(s,startIndex,i)){
                //在s后面插入'.'   使用拼接:[0,i].[i,end]
                s=s.substring(0,i+1)+'.'+s.substring(i+1);
                //分隔符数量+1
                pointNum++;
                //递归下一层,由于添加了.,则下一层从i+2开始而不是i+1
                backTracking(s,i+2,pointNum);
                //回溯
                pointNum--;
                s=s.substring(0,i+1)+s.substring(i+2);
            }
        }
    }
    /*
        有效判断分为四个方面
        1.区间异常
        2.数字0开头
        3.非数字
        4.大于255
    */
    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 num=0;
        for(int i=start;i<=end;i++){
            //非数字
            if(s.charAt(i)>'9'||s.charAt(i)<'0'){
                return false;
            }
            //大于255
            num=num*10+(s.charAt(i)-'0');
            if(num>255){
                return false;
            }
        }
        return true;
    }
    public List<String> restoreIpAddresses(String s) {
        //剪枝
        if(s.length()>12){
            return res;
        }
        backTracking(s,0,0);
        return res;
    }
}

子集问题

无重复集合的子集

在这里插入图片描述
遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。

class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    public void backTracking(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]);
            backTracking(nums,i+1);
            path.removeLast();
        }
    }
    public List<List<Integer>> subsets(int[] nums) {
        backTracking(nums,0);
        return res;
    }
}

包含重复集合的子集

在这里插入图片描述
去重:同一树层的去重,nums[i]=nums[i-1]&&used[i-1]=false,则跳过

class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    static boolean[]used;
    public void backTracking(int []nums,int startIndex,boolean[] used){
        res.add(new ArrayList<>(path));  //防止遗漏本身
        if(startIndex>=nums.length){ 
            return;
        }
        for(int i=startIndex;i<nums.length;i++){
           if(i>=1&&nums[i]==nums[i-1]&&used[i-1]==false){
               continue;
           }
           path.add(nums[i]);
           used[i]=true;
           backTracking(nums,i+1,used);
           used[i]=false;
           path.removeLast(); 
        }
    }

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        used=new boolean[nums.length+1]; //used数组需要声明在这里! 
        Arrays.sort(nums);
        backTracking(nums,0,used);
        return res;
    }
}

递增子序列

在这里插入图片描述
使用map来对同一树层上的结点去重

class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    public void backTracking(int []nums,int startIndex){
        if(path.size()>1){
            res.add(new ArrayList<>(path));
            //不加return,因为要取树结点?
        }
        //使用map来去重
        HashMap<Integer,Integer>hashMap=new HashMap<>();
        for(int i=startIndex;i<nums.length;i++){
        	//保持递增
            if(!path.isEmpty()&&nums[i]<path.getLast()){
                continue;
            }
            //同一树层已经使用过的就跳过
            if(hashMap.getOrDefault(nums[i],0) >=1){
                continue;
            } 
            hashMap.put(nums[i],hashMap.getOrDefault(nums[i],0)+1);
            path.add(nums[i]);
            backTracking(nums,i+1);
            path.removeLast();
        }
    }
    public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums,0);
        return res;
    }
}

排列问题

全排列

在这里插入图片描述
因为是排列问题,所以不用startIndex,用used数组记录已经使用过的

class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    boolean used[];
    public void backTracing(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]){
                continue;
            }
            path.add(nums[i]);
            used[i]=true;
            backTracing(nums,used);
            path.removeLast();
            used[i]=false;
        }
    }
    public List<List<Integer>> permute(int[] nums) {
        used=new boolean[nums.length];
        backTracing(nums,used);
        return res;
    }
}

包含重复数字的全排列

在这里插入图片描述

  1. 去重前要对数组排序,这样才好判断哪些元素重复使用了
  2. 组合问题和排列问题是在树形结构的叶子节点上收集结果,因此需要return
  3. 而子集问题就是取树上所有节点的结果,不需要return。
    在这里插入图片描述
class Solution {
    List<List<Integer>>res=new ArrayList<>();
    LinkedList<Integer>path=new LinkedList<>();
    boolean used[];
    public void backTracing(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]==false){
                continue;
            }
            //从同一树枝上没有使用过开始
            if(used[i]==false){
                used[i]=true;
                path.add(nums[i]);
                backTracing(nums,used);
                used[i]=false;
                path.removeLast();
            }
        }
    }
    public List<List<Integer>> permuteUnique(int[] nums) {
        used=new boolean[nums.length];
        //为了判断重复使用的数,先对数组排序
        Arrays.sort(nums);
        backTracing(nums,used);
        return res;
    }
}

棋盘问题

N皇后

在这里插入图片描述

class Solution {
    List<List<String>>res=new ArrayList<>();
    char[][]chessboard;
    /*
        一共有n行,当前遍历到第row行
    */
    public void backTracking(int n,int row,char[][]chessboard){
        if(n==row){ //结束
            res.add(Array2List(chessboard));
            return;
        }
        for(int col=0;col<n;col++){
            if(isValid(row,col,n,chessboard)){
                chessboard[row][col]='Q';
                backTracking(n,row+1,chessboard);
                chessboard[row][col]='.';
            }
        }
    }
    /*
        同一行,同一列,同一斜线不能有皇后
    */
    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;
            }
        }
        //副对角线
        for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if(chessboard[i][j]=='Q'){
                return false;
            }
        }
        //主对角线
        for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
            if(chessboard[i][j]=='Q'){
                return false;
            }
        }
        return true;
    }

    /*
        将字符数组转化为<List>String
    */
    public List<String>Array2List(char [][]chessboard){
        List<String>list=new ArrayList<>();
        for(char[]c:chessboard){
            list.add(String.copyValueOf(c));
        }
        return list;
    }
    public List<List<String>> solveNQueens(int n) {
        chessboard=new char[n][n];
        //初始化棋盘
        for(char[]c:chessboard){
            Arrays.fill(c,'.');    
        }
        backTracking(n,0,chessboard);
        return res;
    }
}

解数独

在这里插入图片描述

  1. 因为每行都不只填一个数,因此要二维遍历进行递归
  2. 回溯的返回类型是bool型,因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
  3. 本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。 递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
class Solution {
    //二维递归
    private boolean backTracking(char[][]board){
        for(int i=0;i<board.length;i++){
            for(int j=0;j<board[0].length;j++){
                if(board[i][j]=='.'){ //如果还没有填
                   for(char k='1';k<='9';k++){ //试填1~9
                        if(isValid(i,j,k,board)){ //如果合法就填入k
                            board[i][j]=k;
                            if(backTracking(board))return true; //如果递归成功返回保留值
                            board[i][j]='.'; //回溯
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }

    private  boolean isValid(int row,int col,char val,char[][]board){
        //同一列不重复
        for(int i=0;i<9;i++){
            if(board[i][col]==val){ //同一列重复,返回false
                return false;
            }
        }
        //同一行不重复
        for(int j=0;j<9;j++){
            if(board[row][j]==val){
                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]==val){
                    return false;
                }
            }
        }
        return true;
    }
    public void solveSudoku(char[][] board) {
        backTracking(board);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值