回溯算法:组合、分割、子集、排列、棋盘

1、回溯法 = 回溯搜索法 = 穷举(本质)+剪枝  (不高效)

2、回溯函数 = 递归函数    (回溯是递归的副产品,只要有递归就会有回溯)递归必须有终止条件

3、回溯法解决的问题都可以抽象为N叉树形结构(二叉树中经常用到递归),集合的大小就构成了树的宽度,递归的深度,都构成的树的深度

4、回溯算法模板框架如下:for循环横向遍历,递归纵向遍历,回溯不断调整结果集

  • 确定回溯函数的返回值和参数  (在处理过程中确定)
  • 函数结束条件+操作(返回或保存结果等)
  • 函数每层遍历的逻辑:遍历该层的每一个元素 + 处理 + 撤销处理结果(回溯)
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

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

1、, 216. 1-9组合总和(不可重复) III,17. 电话号码的字母组合39. 组合总和(可重复)40. 组合总和 II(不可重复)解集不能包含重复的组合40. 组合总和 II(不可重复)

  • 结束条件:组合完成(列表内有k各元素),保存组合结果,结束、返回
  • 正常情况:依次 从当前可选序列中,添加一个数据,并在当前状态下继续处理数据,处理完后恢复原状(当前层的起始状态)。
    • 解集不能包含重复的组合:N叉树每层for遍历的时候,判断当前数值有没有在该层使用过
      • if(i>index && candidates[i] == candidates[i-1]) continue; //index为当前层的起始位置
      • 前提:candidates数组是有序的(即重复的数值在一起)
      • //同一层中数值相同的元素,不可重复使用,避免组合的重复
    • 组合中同一数值是否可以重复被选取:取决于下一次可选元素的起始位置
      • 可重复:comSum(candidates,target-candidates[i],i);//下次查找范围包含当前值
      • 不可重复:comSum(candidates,target-candidates[i],i+1);//下次查找范围不包含当前值
    • 剪枝操作:数组的选取范围,
      • 当前剩余的元素个数是否满足需要的组合候选元素数
      • 当前的组合值是否已经不满足条件,后续的其他组合不用遍历。前提:有序数组中按序选取。
//找出k 个数的组合
class Solution {
    List<List<Integer>> res = new LinkedList<>();
    List<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        combineTrack(n,k,1);
        return res;
    }
    public void combineTrack(int n, int k, int start){
        if(path.size() == k){//组合完成
            res.add(new LinkedList<>(path));
            return;
        }
        //start 还没包含在组合中,属于查找范围,(start-1) + k-path.size()<= n
        for(int i = start; i <= n-k+path.size()+1; i++){
            path.add(i);
            combineTrack(n,k,i+1);
            path.remove(path.size()-1);//回溯
        }
    }
}


//找出所有相加之和为 n 的 k 个数的组合
class Solution {
    List<List<Integer>> res = new LinkedList<>();
    List<Integer> path =  new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        combine(k,n,1);
        return res;
    }
    public void combine(int k, int target, int start){
        if(target < 0 ) return;
        if(path.size() == k) {
            if(target == 0){
                res.add(new LinkedList<>(path));
            }
            return;
        }
   //利用for循环遍历  (执行时间更短)
        // for(int i = start; i <= 9-(k-path.size()-1); i++){
        //     path.add(i);
        //     target -= i;
        //     combine(k,target,i+1);
        //     target += i;
        //     path.remove(path.size()-1);
        // }

        //递归遍历
        // 剪枝
        if(start-1+k-path.size() > 9)  return;
        //使用当前节点
        path.add(start);
        target -= start;
        combine(k,target,start+1);
        target += start;
        path.remove(path.size()-1);
        // 不使用当前节点
        combine(k,target,start+1);
    }
}

//17. 电话号码的字母组合:仅包含数字 2-9 的字符串,返回所有它能表示的字母组合
class Solution {
    List<String> res = new ArrayList<>();
    //定义映射关系
    String[] numMap = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"}; 
    public List<String> letterCombinations(String digits) {
        if(digits == null || digits.length() == 0) return res;
        StringBuffer letter = new StringBuffer(digits.length());
        buildLetter(digits,0,letter);
        return res;
    }
    public void buildLetter(String digits, int index, StringBuffer letter){
        if(index >= digits.length()){//所有数值对应的字符都已确定
            res.add(letter.toString());
            return;
        }
        int num = digits.charAt(index)-'0';
        String nMap = numMap[num];
        for(int i = 0; i<nMap.length(); i++){
            letter.append(nMap.charAt(i));
            buildLetter(digits,index+1,letter);
            letter.deleteCharAt(letter.length()-1);
        }
    }
}

// candidates 中可以使数字和为目标数 target 的 所有 不同组合,数值可重复使用
//注意区分:组合(无顺序)、排序(有顺序)
class Solution {
    List<List<Integer>> res = new LinkedList<>();
    List<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        comSum(candidates,target,0);
        return res;
    }

    public void comSum(int[] candidates,int target, int index){//index表示元素的选取范围,避免组合间的排序不同内容重复
        if(target == 0){
            res.add(new LinkedList<>(path));
            return;
        }
        for(int i=index; i<candidates.length && candidates[i] <= target;i++){
            if(target-candidates[i] < 0) break;
            path.add(candidates[i]);
            comSum(candidates,target-candidates[i],i);//同一数指可以重复使用,所以范围包括i
            path.remove(path.size()-1);
        }
    }
}

//candidates 中的每个数字在每个组合中只能使用 一次 ,且解集不能包含重复的组合。
class Solution {
    List<List<Integer>> res = new LinkedList<>();
    List<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);为了将重复的数字都放到一起,升序排列,便于去重和剪枝
        comSum(candidates,target,0);
        return res;
    }

    public void comSum(int[] candidates,int target, int index){//index表示元素的选取范围,避免组合间的排序不同内容重复
        if(target == 0){
            res.add(new LinkedList<>(path));
            return;
        }
        for(int i=index; i<candidates.length && candidates[i] <= target;i++){
            if(i>index && candidates[i] == candidates[i-1]) continue;  //同一层中数值相同的元素,不可重复使用,避免组合的重复
            if(target-candidates[i] < 0) break;
            path.add(candidates[i]);
            comSum(candidates,target-candidates[i],i+1);//同一数指不可以重复使用,所以范围不包括i
            path.remove(path.size()-1);
        }
    }
}

2、131. 分割回文串

结束条件:到达字符串结尾,存储结束

层遍历情况: 从当前起始位置,不断的取子字符串,判断该字符串是否为回文字符串,是则存入path,不是则下一字符串。在当前状态下深度遍历+回溯。

class Solution {
    List<List<String>> res = new ArrayList<>();
    List<String> path = new ArrayList<>();
    public List<List<String>> partition(String s) {
        backTracking(s,0);
        return res;
    }
    public void backTracking(String s, int index){
        if(index >= s.length()){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = index; i<s.length(); i++){
            if(isPalindrome(index,i,s)){
                String str = s.substring(index,i+1);//不包含结束索引对应的字符
                path.add(str);
            }else{
                continue;//找下一个符合条件的字符串
            }
            backTracking(s,i+1);//不能重复分割
            path.remove(path.size()-1);
        }
    }

    public boolean isPalindrome(int begin, int end, String str){
        while(begin < end){
            if(str.charAt(begin) != str.charAt(end)) return false;
            begin++;
            end--;
        }
        return true;
    }
}

3、93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。给定一个只包含数字的字符串 s,返回可能的IP地址。

结束条件:

  • 字符串中剩余元素过多或过少,不能构成正确的IP地址,遍历结束
  • 字符串的字符全部遍历过了, 且 IP地址包含4位,保存结果,当前遍历结束

正常层遍历:

  • 从当前起始位置选取字符,判断所选字符串对应的数值是否符合IP要求,不符合则j结束遍历,符合则IP中添加新元素,继续向下遍历,遍历后消除当前操作。
class Solution {
    List<String> res = new ArrayList<>();
    List<String> IP = new ArrayList<>();
    StringBuilder res1;
    public List<String> restoreIpAddresses(String s) {
        res1 = new StringBuilder(s.length()+3);
        backTracking(s,0);
        return res;
    }
    public void backTracking(String s, int index){
        if(s.length() - index < 4 - IP.size()) return;//剩余元素不足
        if(s.length() - index > 3*(4 - IP.size())) return;//剩余元素过多
        //保存结果
        if(index == s.length() && IP.size() == 4){
            for(int i = 0; i < 3; i++){
                res1.append(IP.get(i)+".");
            }
            res1.append(IP.get(3));
            res.add(new String(res1));
            res1.delete(0,res1.length());
        }
        
        for(int i=index; i<index+3 && i<s.length(); i++){//IP地址的每个位置上最多为3位数
            if(i > index && s.charAt(index) == '0') return;//不能含有前导 0,否则当前分割方法不对,返回,注意取出的值为字符
            String str = s.substring(index,i+1);
            int num = Integer.parseInt(str);
            if(num >= 0 && num <= 255){//每个整数位于 0 到 255 之间组成
                IP.add(str);
                backTracking(s,i+1);
                IP.remove(IP.size()-1);
            }else{
                continue;
            }
        }
    }
}

4、 找出所有满足条件的子集(幂集):遍历这个树的时候,把所有满足条件的节点都记录下来,不满足的直接排除,就是要求的子集集合

78. 子集:数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

90. 子集 II:数组可能包含重复元素,请你返回该数组所有可能的子集(幂集)。子集不重复

491. 递增子序列:所有该数组中不同的递增子序列(可含相同数值), 子集至少有两个元素且不重复

//数组中的元素 互不相同 。回该数组所有可能的子集(幂集)。
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        buildSubset(nums,0);
        return res;
    }
    public void buildSubset(int[] nums, int index){
        res.add(new ArrayList<>(path));//树中的每一节点都保存,不需要条件判断
        if(index == nums.length) return;
        for(int i = index; i < nums.length; i++){
            path.add(nums[i]);
            buildSubset(nums, i+1);
            path.remove(path.size()-1);
        }
    }
}

//解集 不能 包含重复的子集(即原集合中有重复元素)
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);//层遍历时去重需要排序
        findSubset(nums,0);
        return res;
    }
    public void findSubset(int[] nums, int index){
        res.add(new ArrayList<>(path));//每一节点都保存结果。
        if(index >= nums.length) return;
        for(int i = index; i < nums.length; i++){
            if(i > index && nums[i] == nums[i-1]) continue;//同一层中不要有重复元素
            path.add(nums[i]);
            findSubset(nums,i+1);
            path.remove(path.size()-1);
        }
    }
}

//所有该数组中不同的递增子序列,递增子序列中至少有两个元素 
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums,0);
        return res;
    }
    public void backTracking(int[] nums, int index){
        if(path.size() >= 2){
            res.add(new ArrayList<>(path));
        }
        for(int i = index; i<nums.length; i++){
            if(!path.isEmpty() && path.get(path.size()-1) > nums[i])
                continue;//不满足递增条件
            if(!find(nums, index, i))
                continue;//不满足子集不重复,层重复
            path.add(nums[i]);
            backTracking(nums,i+1);
            path.remove(path.size()-1);
        }
    }
    public boolean find(int[] nums, int index, int i){
        for(int j = index; j<i; j++){
            if(nums[i] == nums[j]) return false;
        }
        return true;
    }
}

5、排列:有顺序区分,除路径中的已有元素其他元素局可选+去层重复,利用标记数组去重

46. 全排列:不含重复数字的数组,返回其所有可能的全排列。可以通过路径是否已经包含该元素,进行判断当前元素是否可以使用。不存在重复子集

47. 全排列 II:可包含重复数字的序列,返回所有不重复的全排列。

  • 层内不重复:数组排序,层遍历中,nums[i-1] == nums[i],且nums[i-1]已经遍历过了,去除
  • 路径中不同元素可重复:nums[i]使用过了,去除。使用used数组进行标记
//不含重复数字的数组nums ,返回其 所有可能的全排列
class Solution {
     List<List<Integer>> res = new ArrayList<>();
     List<Integer> path = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        backTracking(nums);
        return res;
    }
    public void backTracking(int[] nums){//除路径中已有元素,其他元素均可选,不需要设置选取范围
        if(path.size() == nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = 0; i<nums.length; i++){
            if(path.contains(nums[i])) continue;
            path.add(nums[i]);
            backTracking(nums);
            path.remove(path.size()-1);
        }
    }
}

// 可包含重复数字的序列 nums,  返回所有不重复的全排列。
// 注意 如何去重
class Solution {
    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.fill(used,false);
        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] == false) continue;//层重复
            if(used[i] == true) continue;//在路径中使用过了
            path.add(nums[i]);
            used[i] = true;
            backTrack(nums,used);
            used[i] = false;
            path.remove(path.size()-1);
        }
    }
}

6、棋盘:遍历棋盘各个位置,判断可放置元素的合法性,合法则放置并判断之后的情况,不行则回溯。可根据当前状态的返回值,判断是否需要继续回溯遍历,返回true,表示在当前状态下其他位置均合理。

51. N 皇后:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。如何使N个皇后彼此之间不能相互攻击。

class Solution {
    List<List<String>> res = new LinkedList<>();
    public List<List<String>> solveNQueens(int n) {
        char[][] chess = new char[n][n];
        for(char[] c: chess){
            Arrays.fill(c,'.');
        }
        helpNQueens(chess, n, 0);
       
        return res;
    }

    public void helpNQueens(char[][] chess, int n, int row){
        if(row == n){
            List<String> res1 = new ArrayList<>();
            for(char[] c: chess){
                res1.add(String.copyValueOf(c));
            }
            res.add(new LinkedList<>(res1));
            return;
        }
        for(int i = 0; i < n; i++){//每行的每列
            if(isValid(chess, n, row, i)){//判断位置是否合法
                chess[row][i] = 'Q';
                helpNQueens(chess, n, row+1);
                chess[row][i] = '.';
            }
        }
    }

    public boolean isValid(char[][] chess, int n, int row, int col){
        //同行,不可能,一行只放一个
        //同列
        for(int i = 0; i< row; i++){
            if(chess[i][col] == 'Q')
                return false; 
        }
        //同一斜线,主对角线
        for(int i = row-1,j = col-1; i>=0 && j>=0; i--,j--){
            if(chess[i][j] == 'Q')
                return false;         
        }
        //同一斜线,副对角线
        for(int i = row-1, j = col+1; i>=0 && j<n; i--,j++){
            if(chess[i][j] == 'Q')
                return false;
        }
        return true;
    }
}

51. N 皇后: 填充空格来解决数独问题。数独的解法需 遵循如下规则:

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。

class Solution {
    public void solveSudoku(char[][] board) {
        helpSolveSudoku(board);
    }
    public boolean helpSolveSudoku(char[][] board){
        for(int i = 0; i< board.length; i++ ){
            for(int j = 0; j< board[0].length; j++){
                if(board[i][j] != '.') continue;
                for(char v = '1'; v <= '9'; v++){//存放的是字符
                    if(isvalid(board,i,j,v)){
                        board[i][j] = v;
                        if(helpSolveSudoku(board)) return true;//找到正确的值就一层层向上告知
                        board[i][j] = '.';
                    }
                }
                return false;//9个数均不合适,说明前面排列错误;返回
            }  
        }
        return true;//已正确填充
    }
    
    public boolean isvalid(char[][] board, int row, int col, char v){
        // 行内不重复
        for(int j = 0; j<board.length; j++){
            if(board[row][j] == v) return false;
        }
        // 列内不重复
        for(int i = 0; i<board[0].length; i++){//一行元素的长度:列数
            if(board[i][col] == v) return false;
        }
        // 九宫格内不重复
        for(int i = row/3 *3; i<(row/3+1)*3; i++){
             for(int j = col/3 *3; j<(col/3+1)*3; j++){
                if(board[i][j] == v) return false;
             }
        }
        return true;
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值