一,回溯题的常用模板:
注意模板:在选择之前需要排除不合法的选择!!!!
注意体会做选择和撤销选择
如下面的dfs所示:
★向一路子节点一直进行遍历,直到子节点为null,此时就要进行return了,一直return到某个拥有右子节点的子节点,这个return的过程便是回溯。
★回溯使我们可以遍历所有的子节点(即可以遍历所有的情况),所以,我们是不是可以使用回溯来进行一些类似的操作(全排列,子集,组合问题)
下面情况均分需不需要index和i++来进行讨论。
二,普通的回溯算法
1.电话号码的字母组合
★需要index,需要i++情况
只是比普通的回溯算法加多了对数据的处理而已
注意使用字符串时(StringBuilder添加字符和删除字符的操作)
该题需要记录index来进行判断,而且字符不可能重复,故不需要进行去重的操作,i从0开始即可
class Solution {
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList();
if (digits.length() == 0) {
return res;
}
//将所有可能的组合放入哈希表中
Map<Character,String> map = new HashMap() {
{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
} };
backTrace(res, map, digits,0, new StringBuilder());
return res;
}
public static void backTrace(List<String> res,Map<Character,String> map,String digits,int index,StringBuilder re){
//简单的basecase
if(index == digits.length()){
res.add(re.toString());
return;
}
//获取当前数组对应的字母
char c = digits.charAt(index);
String s = map.get(c);
int l = s.length();
//对所有可能做选择
for(int i = 0; i < l;i++){
re.append(s.charAt(i));
backTrace(res,map,digits,index + 1,re);
re.deleteCharAt(index);
}
}
}
2.组合总和
★需要index,不需要i++情况
该题中:2+3+2和3+2+2是一样的,故需要从index开始,而且一个数字可以使用多次(即做选择的时候,该选择可选择多次,不需要下次判断时排除掉),故不需要i++;
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
backTrace(res,candidates,target,0,re);
return res;
}
public static void backTrace(List<List<Integer>> res,int[] candidates,int target,int index,List<Integer> re){
if(target < 0){
return;
}
if(target == 0){
res.add(new ArrayList<>(re));
return;
}
for(int i = index; i < candidates.length;i++){
re.add(candidates[i]);
backTrace(res,candidates,target - candidates[i],i,re);
re.remove(re.size() - 1);
}
}
}
三,回溯的去重问题
1.组合总和②
★需要index,不需要i++;
上一题的进阶版,难点为去重,下面是我一开始的错误思路,使用set来去重,但只是去掉了数字的重复,组合的重复并没有解决
原因:我们都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一个树枝上使用过,一个维度是同一树层上使用过。
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一组合里的元素,不用去重。(树层去重是需要对数组进行排序的)
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
HashSet<Integer> set = new HashSet();
backTrack(res,target,candidates,0,re,set);
return res;
}
public void backTrack(List<List<Integer>> res,int target,int[] candidates,int index,List<Integer> re,HashSet<Integer> set){
if(target < 0){
return;
}
if(target == 0){
set.clear();
res.add(new ArrayList<>(re));
}
for(int i = index; i < candidates.length;i++){
if(!set.contains(candidates[i])){
re.add(candidates[i]);
set.add(candidates[i]);
}else{
continue;
}
backTrack(res,target - candidates[i],candidates,i,re,set);
re.remove(re.size() - 1);
}
}
}
正解:注意一定要先对数组进行排序
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
Arrays.sort(candidates);
backTrack(res,target,candidates,0,re);
return res;
}
public void backTrack(List<List<Integer>> res,int target,int[] candidates,int index,List<Integer> re){
if(target < 0){
return;
}
if(target == 0){
res.add(new ArrayList<>(re));
}
for(int i = index; i < candidates.length;i++){
if(candidates[i]<=target){
if(i>index&&candidates[i]==candidates[i-1]){
continue;
}
re.add(candidates[i]);
backTrack(res,target - candidates[i],candidates,i + 1,re);
re.remove(re.size() - 1);
}
}
}
}
重点体会:
这里使用了index,就不需要使用used数组了
if(i>index&&candidates[i]==candidates[i-1]){
continue;
}
前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
此时for循环里就应该做continue的操作。
如图所示:
将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
- used[i - 1] == true,说明同一树支candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
这块去重很抽象,很少题解能够讲清楚,更多的是自己的思考
四,全排列问题
1. 全排列
★不需要index,不需要i++;
我的错误解法:
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
int[] use = new int[nums.length];
if(nums.length == 1){
re.add(nums[0]);
res.add(re);
return res;
}
backTrace(res,re,nums);
return res;
}
public void backTrace(List<List<Integer>> res, List<Integer> re,int[] nums){
if(re.size() == nums.length){
res.add(new ArrayList(re));
return;
}
for(int i = 0;i < nums.length;i++){
re.add(nums[i]);
backTrace(res,re,nums);
re.remove(re.size() - 1);
}
}
}
选择从i = 0 开始,每次选择都选择全部,故有误
正确答案就是在上面的思路上,添加一个used表,把当前选择中已经选择过的排除,就只能选择剩下的数字,而不会出现重复的情况(前提:题目为给出不重复的数字,有重复就不行)
错误使用index也会出现错误哦
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
int[] used = new int[nums.length];
if(nums.length == 1){
re.add(nums[0]);
res.add(re);
return res;
}
backTrace(res,re,nums,0);
return res;
}
public void backTrace(List<List<Integer>> res, List<Integer> re,int[] nums,int index){
if(re.size() == nums.length){
res.add(new ArrayList(re));
return;
}
for(int i = index;i < nums.length;i++){
re.add(nums[i]);
backTrace(res,re,nums,i);
re.remove(re.size() - 1);
}
}
}
那么如果出现重复数字又该如何全排列呢?
2.全排列②
思路其实和上面的去重一样(记得先排序)
class Solution {
// 创建一个全局变量集合,保存结果
List<List<Integer>> alist=new ArrayList<List<Integer>>();
// 讲数组的长度变为全局变量
int len;
public List<List<Integer>> permuteUnique(int[] nums) {
this.len=nums.length;
// 创建一个贯穿整个递归过程的集合
ArrayList<Integer> all=new ArrayList<Integer>();
/*
下面这个想法打错特错这样会导致最后一层的元素肯定都是被标记过的导致无元素可加
好像也不是完全错只是我忽略了数组也是引用类型,返回上一层时,应撤销在本层的操作
// 想到一个解决方法:参数中加上一个visited数组,将该层已经访问过的数子标记
*/
int[] visited=new int[len];
dfs(nums,visited,all,0);
return alist;
}
public void dfs(int[] nums,int[] visited,ArrayList list,int index){
// 出口
if(list.size()==len){
// 因为list是引用变量所以需要创建一个新的集合来保存list中的数据
ArrayList<Integer> temp=new ArrayList<Integer>(list);
alist.add(temp);
return ;
}
// 将符合条件的集合加入到alist中
for(int i=0;i<len;i++){
if(visited[i]==1){
continue;
}
// 如果是没有访问过的就将它加入集合
list.add(nums[i]);
visited[i]=1;
dfs(nums,visited,list,index+1);
// 回退到上一层之前将本层加入的数据给去掉
list.remove(list.size()-1);
visited[i]=0;
}
}
}
//上面的代码解决了自己引用自己的问题,可是在同一层有重复元素怎么办
// 例如 [1,1,2] 上面的代码可以得出
//[[1,1,2],[1,2,1],[1,1,2],[1,2,1],[2,1,1],[2,1,1]]
// 可它有两个重复的1,就那第1层说是,他们两肯定可以获得重复的结果,怎么解决
// 大佬给出了解决方法:先对数组进行排序,就是如果该元素的前面有和它一样的元素,那这个元素就别用了
class Solution {
// 创建一个全局变量集合,保存结果
List<List<Integer>> alist=new ArrayList<List<Integer>>();
// 讲数组的长度变为全局变量
int len;
public List<List<Integer>> permuteUnique(int[] nums) {
this.len=nums.length;
// 创建一个贯穿整个递归过程的集合
ArrayList<Integer> all=new ArrayList<Integer>();
/*
下面这个想法打错特错这样会导致最后一层的元素肯定都是被标记过的导致无元素可加
好像也不是完全错只是我忽略了数组也是引用类型,返回上一层时,应撤销在本层的操作
// 想到一个解决方法:参数中加上一个visited数组,将该层已经访问过的数子标记
*/
int[] visited=new int[len];
// 对数组进行排序
Arrays.sort(nums);
dfs(nums,visited,all,0);
return alist;
}
public void dfs(int[] nums,int[] visited,ArrayList list,int index){
// 出口
if(list.size()==len){
// 因为list是引用变量所以需要创建一个新的集合来保存list中的数据
ArrayList<Integer> temp=new ArrayList<Integer>(list);
alist.add(temp);
return ;
}
// 将符合条件的集合加入到alist中
for(int i=0;i<len;i++){
// visit解决了自己访问自己的问题,并且
if(visited[i]==1){
continue;
}
// 现在解决同一层出现重复元素的问题
// 为什么要加上这个?visited[i-1]==0
// 解释在下面
if(i>0&&nums[i]==nums[i-1] && visited[i-1]==0){
continue;
}
// 如果是没有访问过的就将它加入集合
list.add(nums[i]);
visited[i]=1;
dfs(nums,visited,list,index+1);
// 回退到上一层之前将本层加入的数据给去掉
list.remove(list.size()-1);
visited[i]=0;
}
}
}
/*
大神的理解:for循环保证了从数组中从前往后一个一个取值,再用if判断条件。所以nums[i - 1]一定比nums[i]先被取值和判断。如果nums[i - 1]被取值了,那vis[i - 1]会被置1,只有当递归再回退到这一层时再将它置0。每递归一层都是在寻找数组对应于递归深度位置的值,每一层里用for循环来寻找。所以当vis[i - 1] == 1时,说明nums[i - 1]和nums[i]分别属于两层递归中,也就是我们要用这两个数分别放在数组的两个位置,这时不需要去重。但是当vis[i - 1] == 0时,说明nums[i - 1]和nums[i]属于同一层递归中(只是for循环进入下一层循环),也就是我们要用这两个数放在数组中的同一个位置上,这就是我们要去重的情况。
可以好好的遍历一次就会理解该思路了,遍历时主要是要理解,第2层的元素,在第三层所有的元素被访问后,就会回溯
到第一层,回到第一层后所有的元素是没有标记的,所以去重一定要加上visited[i-1]==0这样才说明他们才是在同一层
[1,1,2]
[1,1,2]
[1,1,2]
*/
★重点
上面记录了这么多需不需要index,那到底上面时候需要呢?
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不能重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
那什么时候for可以从0开始呢?
求排列组合问题的时候,就要从0开始,因为排列组合是有序的,{1, 2} 和{2, 1}是两种排序
那什么时候需要i++呢,无序时需要,具体看能不能重复!
1.组合
这题出现的是组合,[1,4],[4,1]的组合是一样的,该题目是无序的,故需要从index开始使i++
正解为:
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
backTrace(res,re,n,k,1);
return res;
}
public void backTrace(List<List<Integer>> res,List<Integer> re,int n,int k,int index){
if(re.size() == k){
res.add(new ArrayList(re));
return;
}
for(int i = index; i <= n; i++){
re.add(i);
backTrace(res,re,n,k,i + 1);
re.remove(re.size() - 1);
}
}
}
2.子集
★易错点
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
ArrayList<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
//先排序
Arrays.sort(nums);
//定义一个数组判断是否已经访问过了
int n = nums.length;
int[] used = new int[n];
if(nums == null){
return res;
}
backTrace(res,re,n,used,nums,0);
return res;
}
//子集问题是无序的,故需要一个index,已确保取过的元素不会重复再取
public void backTrace(ArrayList<List<Integer>> res,List<Integer> re,int n,int[] used,int[] nums, int index){
//子集问题,不需要basecase
res.add(new ArrayList(re));
for(int i = index; i < n; i++){
if(i > 0&&nums[i] == nums[i - 1] && used[i - 1] == 0){
continue;
}
re.add(nums[i]);
used[i] = 1;
backTrace(res,re,n,used,nums,index + 1);
re.remove(re.size() - 1);
used[i] = 0;
}
}
}
这是在回溯时,回溯条件上写index + 1
提交后的答案
正解为使用i +1
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
ArrayList<List<Integer>> res = new ArrayList();
List<Integer> re = new ArrayList();
//先排序
Arrays.sort(nums);
//定义一个数组判断是否已经访问过了
int n = nums.length;
int[] used = new int[n];
if(nums == null){
return res;
}
backTrace(res,re,n,used,nums,0);
return res;
}
//子集问题是无序的,故需要一个index,已确保取过的元素不会重复再取
public void backTrace(ArrayList<List<Integer>> res,List<Integer> re,int n,int[] used,int[] nums, int index){
//子集问题,不需要basecase
res.add(new ArrayList(re));
for(int i = index; i < n; i++){
if(i > 0&&nums[i] == nums[i - 1] && used[i - 1] == 0){
continue;
}
re.add(nums[i]);
used[i] = 1;
backTrace(res,re,n,used,nums,i + 1);
re.remove(re.size() - 1);
used[i] = 0;
}
}
}