回溯法小结及相关刷题小结

回溯法

回溯法,属于DFS,也是暴力遍历。对所有的可能结果进行组合。普通的DFS属于求解可达性的问题,这种问题执行到特定的位置,然后返回即可。BackTracking则用于求解排列组合的问题。

回溯求解时候,不会立刻返回,而是要继续求解,因此在程序实现种,需要对元素进行标记

  • 在访问一个新元素进入递归的调用的时候,需要将新元素标记为已访问,这样才能在接下来调用时候不必重复访问。
  • 在递归返回的时候,需要将元素标记为未访问,因为只需要保证在一个递归链种不同时访问一个元素即可。可以访问已经访问过但是不在本递归链中的元素。

回溯法解决的问题有以下几类:

数的排列和组合

数字的排列

给出数组,求出数组中所有数的排列结果。Leetcode中的46和47题是典型例子。46和47的差别在于,47题中的数组中含有重复元素,需要去重复处理。

46. 全排列 - 力扣(LeetCode) (leetcode-cn.com)

47. 全排列 II - 力扣(LeetCode) (leetcode-cn.com)

package JavaCode.leetcode.Algorithm.Search.BackTracing.permute;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/*
全排列。
给定一个数组,返回所有可能的全排列。所有的整数互不相同
 */
public class Q46 {
 
    public static List<List<Integer>> permute(int[] nums) {
          List<List<Integer>> list=new ArrayList<List<Integer>>();
          if(nums==null||nums.length==0)return null;
            List<List<Integer>> permutation=new ArrayList<List<Integer>>();
            List<Integer> prefix=new ArrayList<>();//用于存储每一次排列。
            boolean[] hasVisited=new boolean[nums.length];//用于标记是否访问使用过
            //寻找所有排列时候,每次选中一个数后,这个数就不能再出现在排列中,故而应该
            //用一个数组来表示是否已经访问过此元素
            BackTacking(prefix,permutation,nums,hasVisited);
            return permutation;
    }

    public static void BackTacking(List<Integer> prefix,List<List<Integer>> Permutation,int[] nums,boolean[] hasVisited){
        if(prefix.size() == nums.length){
            Permutation.add(new ArrayList<>(prefix));//添加列表时,先要实例化.
            return;//添加后终止返回。
        }
        for (int i = 0; i < nums.length; i++) {
            if(hasVisited[i]){
                continue;
            }
            prefix.add(nums[i]);
            hasVisited[i]=true;//加入列表后标记为true,在本次递归链中,不能再访问这个元素
            BackTacking(prefix,Permutation,nums,hasVisited);
            prefix.remove(prefix.size()-1);//递归结束,删除,表示之后的其他递归链可以访问本元素
            hasVisited[i]=false;//删除后标记为false
        }
    }
}
package JavaCode.leetcode.Algorithm.Search.BackTracing.permute;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
/*
对含有重复数字的数组进行全排列
 */
public class Q47 {
    public static List<List<Integer>> permuteUnique(int[] nums) {
          if(nums==null||nums.length==0)return null;
          List<List<Integer>> permute=new ArrayList<List<Integer>>();
          List<Integer> prefix=new ArrayList<>();
          //为了在之后的回溯中更方便的处理重复的情况,首先要将数组进行排序,排序之后的数组重复元素会相邻
          Arrays.sort(nums);
          boolean[] hasVisited=new boolean[nums.length];
          BackTracking(prefix,permute,nums,hasVisited);
          return permute;
    }
    public static void BackTracking(List<Integer> prefix, List<List<Integer>> permute,int[] nums,boolean[] hasVisited){
          if(prefix.size()== nums.length){
              permute.add(new ArrayList<>(prefix));
              return;
          }
        for (int i = 0; i < nums.length; i++) {
            if(i!=0&&nums[i-1]==nums[i]&&!hasVisited[i-1]){
                continue;
                /*
                当前的数与前一个数相同,且前一个数未访问时,说明前一个数已经在某轮组合中使用
                然后重置为false,此时应该跳过这个数
                如果前一个数是访问过的,说明此时处于某一轮排列寻找中,该数可以被加入
                 */
            }
            //当前的数字已经访问过,就跳过
            if(hasVisited[i]){
                continue;
            }
            prefix.add(nums[i]);
            hasVisited[i]=true;//加入列表后标记为true
            BackTracking(prefix,permute,nums,hasVisited);
            prefix.remove(prefix.size()-1);
            //用size-1索引,用indexOf找索引再删除,如果有重复元素,这个方法失效
            hasVisited[i]=false;//删除后标记为false
        }
    }
}

数字的组合问题

Leetcode中关于数字组合的问题主要有77,39,40,216。数字的组合与数字的在序列中的位置无关。{1,2,3},{2,3,1}就被认为是一个相同的组合,但却是两个不同的排列。

77题

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

你可以按 任何顺序 返回答案。

package JavaCode.leetcode.Algorithm.Search.BackTracing.combine;

import java.util.ArrayList;
import java.util.List;
/*
给定两个整数n和k,从1-n中选出k个数的组合。其中n的范围[1,20]
k[1,n]
 */
public class Q77 {
    public static List<List<Integer>> combine(int n, int k) {
        boolean[] visited=new boolean[n];
        List<List<Integer>> combine = new ArrayList<>();
        List<Integer> combineList = new ArrayList<>();
        BackTracking(combineList,combine,1,k,n);
        /*
        实现:将从start开始到n中选取k个数进行组合
         */
        return combine;
    }
/*
回溯:找到从start到n中的k个数,直到k等于0
 */
    public static void BackTracking(List<Integer> combineList, List<List<Integer>> combine, int start, int k, int n) {
        if (k==0) {
            combine.add(new ArrayList<>(combineList));
            return;
        }
        for (int i = start; i <=n-k+1 ; i++) {
            combineList.add(i);//加入
            BackTracking(combineList, combine, i+1, k-1, n);//i+1后,上一个元素便不会被重复使用
            combineList.remove(combineList.size()-1);//删除
        }
    }
}

39题

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。

对于给定的输入,保证和为 target 的唯一组合数少于 150 个。

package JavaCode.leetcode.Algorithm.Search.BackTracing.combine;

import java.util.ArrayList;
import java.util.List;
/*
和等于target的组合数,数组中每个元素可以被无限次选用
数组元素均为正数,且各不相同。
 */
public class Q39 {
    public static List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates==null||candidates.length==0)return null;
        List<Integer> combinationList=new ArrayList<>();
        List<List<Integer>> list=new ArrayList<>();
        backTracking(list,combinationList,candidates,target,0);
        return list;
    }
/*
回溯求组合,一般情况要求一个范围内的组合数,通过改变开始和结束范围来递归(此时元素不可以重复使用)
当元素可以重复使用时,每次组合时,每个元素只能作为首个元素出现一次,也可以通过设置start来实现
 */
    public static void backTracking(List<List<Integer>> list,List<Integer> combinationList,int[] candidates,int target,int start){
        //找到了和为目标的组合,添加进List后返回。
        if(target==0){
            list.add(new ArrayList<>(combinationList));
            return;
        }
        //没有找到且已经过界,返回。
        if(target<0){
            return;
        }
        //通过设置start值和递归时候值的改变来觉得是否可以重复使用数组元素
        for (int i =start; i < candidates.length; i++) {
            combinationList.add(candidates[i]);
            //此处递归的下一轮的递归start值设置为i,那么元素可以重复使用。
            //如果设置为i+1,表示每一递归层的元素不能被重复使用
            backTracking(list, combinationList, candidates, target-candidates[i],i);
            combinationList.remove(combinationList.size()-1);
        }
    }
}

40题

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

candidates 中的每个数字在每个组合中只能使用一次。注意:解集不能包含重复的组合。

package JavaCode.leetcode.Algorithm.Search.BackTracing.combine;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
/*
找出给定数组Candidates中组合等于target的组合
数组和目标元素都为正整数
和39不同的地方在于39中元素可以重复使用,而本题目中元素不能重复使用,
39中的元素无重复,本题目中元素可以重复
 */
public class Q40 {}
     //去重复的目的是使得相同的数只作为开头元素出现1次
    public static List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if(candidates==null||candidates.length==0)return null;
        Arrays.sort(candidates);
        List<Integer> combinationList=new ArrayList<>();
        List<List<Integer>> list=new ArrayList<>();
        boolean[] visited=new boolean[candidates.length];

        backtracking(list,combinationList,candidates,0,target,visited);
        return list;
    }
    public static void backtracking(List<List<Integer>> list,List<Integer> combinationList,int[] candidates,int start,int target,boolean[]  visited){
       if(target==0){
           list.add(new ArrayList<>(combinationList));
           return;
       }
        for (int i = start; i < candidates.length; i++) {
            if(i!=0&&candidates[i-1]==candidates[i]&&!visited[i-1]){
                continue;
            }
            /*
            以某一个元素开头去找组合,不能重复,否则会得到重复的数组合。上述语句可以避免使用重复的元素开头
             */
            if(candidates[i]<=target) {
                combinationList.add(candidates[i]);
                visited[i] = true;
                backtracking(list, combinationList, candidates, i + 1, target - candidates[i], visited);
                //递归栈,i+1是因为不能重复使用某个元素
                visited[i] = false;
                combinationList.remove(combinationList.size() - 1);
            }
        }
    }
}

对比39和40,用数组来标记是否访问的目的是使得相同的数只以开头的位置出现1次。

216题

package JavaCode.leetcode.Algorithm.Search.BackTracing.combine;
import java.util.ArrayList;
import java.util.List;
public class Q216 {
    public static List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> combinations=new ArrayList<>();
        List<Integer> combination_list=new ArrayList<>();
        backtracking(combinations,combination_list,1,k,n);
          return combinations;
    }
    public static void  backtracking(List<List<Integer>> combinations,List<Integer> combination_list,int start,int k,int n){
        //个数与目标和均达到条件,添加进入组合中,返回。
        if(k==0&&n==0){
          combinations.add(new ArrayList<>(combination_list));
          return;
        }
        //越界的情况就直接返回。
        if(k<0||n<0)return;
        for (int i = start; i <= 9; i++) {
            combination_list.add(i);
            backtracking(combinations, combination_list, i+1, k-1, n-i);
            //start=i+1,每次就可以避免重复。k和n分别控制个数和数目
            combination_list.remove(combination_list.size()-1);
        }
    }
}

求子集的问题

Leetcode中的78题和90题是求子集的典型问题。

78题78. 子集 - 力扣(LeetCode) (leetcode-cn.com)

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

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

这个问题实质上可以转化为从给定数组中选取0,1,2,3...k,k=nums.length个数进行组合的所有结果。只需要在77题目基础上进行改进即可。求出每个长度目标下的组合。代码如下:

import java.util.ArrayList;
import java.util.List;
/*
nums中所有元素互不相同。
 */
public class Q78 {
    //求子集的问题,就相当于是从给定数组中选出k=0,2,3,...len个数来进行组合。
    public  static List<List<Integer>> subsets(int[] nums) {
       List<List<Integer>> list=new ArrayList<>();
       List<Integer> subset=new ArrayList<>();
       int len= nums.length;
        //对每一种长度进行求组合
        for (int i = 0; i <= len; i++) {
            backtracking(0,list,subset,i,nums);
        }
       return list;
    }
    public static void  backtracking(int start,List<List<Integer>> list, List<Integer> subset ,int size,int[] nums){
        //子集的长度等于目标长度,添加进List,并且终止返回。  
        if(size==subset.size()){
              list.add(new ArrayList<>(subset));
              return;
          } 
          for (int i = start; i < nums.length; i++) {
            subset.add(nums[i]);
            backtracking(i+1, list, subset, size, nums);
            subset.remove(subset.size()-1);
          }
    }
}

90题

90. 子集 II - 力扣(LeetCode) (leetcode-cn.com)

90与78大同小异。区别在于90中给定的数组元素存在重复值。存在重复值就需要进行去重处理以下。

其他问题

除了数字的排列组合以及子集问题。其他可以用回溯解决的问题还有很多。这些问题的特点是本质上都是对元素的排列组合,只不过迁移了问题的背景,加入了其他的知识点。简单举几个例子,Leetcode17题,93题。

17题:电话号码的组合

17. 电话号码的字母组合 - 力扣(LeetCode) (leetcode-cn.com)

具体题目叙述见链接。

给定的数字字符串,每一个数字字符都和字母字符串存在一个对应关系。根据给定的数字字符串可以找到对应的字母字符串

package JavaCode.leetcode.Algorithm.Search.BackTracing.combine;

import java.util.ArrayList;
import java.util.List;
public class Q17 {
    //0和1都是空
    public String[] Keys={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    public static List<String> letterCombinations(String digits) {
        List<String> combinations=new ArrayList<String>();
        if(digits==null||digits.length()==0)return combinations;//特殊情况的处理
        //对给定数的情况下所能组成的字符串进行回溯得到所有解
        BackTracking(new StringBuilder(),combinations,digits);
        return combinations;
    }
    /*
    前缀字符串prefix,回溯返回的条件时,前缀字符串长度==digits.length,说明已经完成一次查找组合
    将此时的前缀转为字符串加入combinations
    若没有到达,就根据此时prefix.length查找到本层回溯应该访问的keys
    对于每一层的Keys,访问到后,对每一个keys将其加入到prefix中,然后继续回溯下一层,回溯结束后在本层应该将
    加入的keys删除
     */
    public static void BackTracking(StringBuilder prefix,List<String>combinations,String digits){
          //前缀和等于给定的数长度时,将这个字符串加入
          if(prefix.length()==digits.length()){
              combinations.add(prefix.toString());
              return;
          }
          int cur=digits.charAt(prefix.length())-'0';//找到此层中应该访问的字符串索引
          String letters=Keys[cur];
          for (char c:letters.toCharArray()) {
            prefix.append(c);//加入
            BackTracking(prefix,combinations,digits);
            prefix.deleteCharAt(prefix.length()-1);//删除
          }
    }
}

93题:IP地址划分问题
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 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 地址。

解析:一个合法的IP地址是用点分十进制表示的且有4段。添加“.”将字符串分割为IP地址。需要一个StringBuilder类来实现字符串的重新拼接。

使用回溯进行分割,最终划分结束时候,有四段字符串且最终进入回溯函数的字符串长度为0,这个是终止判断条件。其余具体操作,见代码注释

import java.util.ArrayList;
import java.util.List;
/*
划分IP地址,IP地址是0-255的四个整数构成,也就是说最多划分的段数k=4
字符串的拼接,常用一个StringBuilder
 */
public class Q93 {
    public static void main(String[] args) {
        String s="25525511135";
        List<String> ans=new ArrayList<>();
        ans=restoreIpAddresses(s);
        System.out.println(ans);
    }
    public static List<String> restoreIpAddresses(String s) {
              StringBuilder sb=new StringBuilder();
              List<String> list=new ArrayList<>();
              part(s,list,0,sb);
              return list;
    }
    /*
    sb为空的时候,不需要添加'.',其余时候需要添加
     */
    public static void part(String s,List<String> list,int k,StringBuilder sb){
        /*
        段数达到4或者此时的字符串长度为0了,说明已经到达末尾,需要判断是否可以终止并添加字符串
         */
        if(k==4||s.length()==0){
             if(k==4&&s.length()==0){
                 list.add(sb.toString());//两个条件同时满足就添加返回
             }
            return;//不同时满足,说明所构成的IP划分结果不符合要求,直接返回,不添加
        }
        for (int i = 0 ; i<s.length()&&i<=2 ;i++) {
            String temp = s.substring(0, i + 1);//获取一个字串
            if(!isValid(temp))continue;//判断子串是否有效
            if (sb.length() != 0) {
                temp = '.' + temp;//sb为0的时候,不需添加.,不为0要加.
            }//根据此时sb类的长度对待添加的字符串进行修改。
            //进入递归函数,,每次进入时,新的字符串是原来的子串,段的计数值+1
                sb.append(temp);
                part(s.substring(i+1), list, k + 1, sb);//字串进入,长度+1
                sb.delete(sb.length() - temp.length(), sb.length());//删除指定范围内的字符串
        }
    }
    /*
    判断划分的字符串是不是合法值的方法
     */
    public static boolean isValid(String s){
        if(s.charAt(0)=='0'&&s.length()>=2)return false;
        if(Integer.valueOf(s)<0||Integer.valueOf(s)>255)return false;
        return true;
    }
}
posted @ 2021-09-26 20:45  汪小川  阅读(139)  评论(0)    收藏  举报