回溯法小结及相关刷题小结
回溯法
回溯法,属于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题
给定两个整数 n 和 k,返回范围 [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;
}
}

浙公网安备 33010602011771号