目录
前言
总结回溯算法的题型
按照规模将其题型都熟练掌握
学习的链接大框架为:
回溯算法理论基础
LinkedList<Integer> sonlist=new LinkedList<>();
双向链表可以删除最前或者添加最前的节点,删除的时候通过removeLast();List<Integer> sonlist=new ArrayList<>();
单链表只能添加,或者删除,删除的时候通过remove(下标值)
组合
77. 组合(中等)
题目:
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
示例 2:
提示:
1 <= n <= 20
1 <= k <= n
思路:
class Solution {
List<List<Integer>> list =new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
if (k <= 0 || n < k) {
return res;
}
backtrace(n,k,1);
return list;
}
public void backtrace(int n,int k,int startindex){
if(sonlist.size()==k){
list.add(new ArrayList<>(sonlist));
return;
}
for(int i=startindex;i<=n;i++){
sonlist.addLast(i);
backtrace(n,k,i+1);
//不用带下标
sonlist.removeLast();
}
}
}
也可以将以上的双向列表换为如下:Deque<Integer> sonlist=new ArrayDeque<>();
进一步的优化,将其for条件的循环更改为:
for(int i=startindex;i<=n-(k-sonlist.size())+1;i++){
这是因为如果 n = 7, k = 4,从 5 开始搜索就已经没有意义了
优化过程:
已经选择的元素个数:sonlist.size();
还需要的元素个数为: k - sonlist.size();
在集合n中至多要从该起始位置 : n - (k - sonlist.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
而且有个错误点:
for(int i=startindex;i<=n-(k-sonlist.size())+1;i++){
sonlist.add(i);
backtrace(n,k,i+1);
sonlist.remove(sonlist.size()-1);
}
内部结构不能使用list列表,即使删除节点也会删除错误,必须要用临时变量记住就会比较麻烦
216. 组合总和 III(中等)
题目:
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示:
2 <= k <= 9
1 <= n <= 60
思路:
计算其sum值,可以将其sum值也遍历,不用额外定义一个int sum在函数外。
注意其终止条件
class Solution {
List<List<Integer>> list=new ArrayList<>();
LinkedList<Integer> sonlist =new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtrace(k,n,1,0);
return list;
}
public void backtrace(int k,int n,int startindex,int sum){
if(sum>n)return;
if(sonlist.size()==k){
if(sum==n)list.add(new ArrayList<>(sonlist));
return;
}
for(int i=startindex;i<=9-(k-sonlist.size())+1;i++){
sonlist.addLast(i);
sum=sum+i;
backtrace(k,n,i+1,sum);
sonlist.removeLast();
sum=sum-i;
}
}
}
39. 组合总和(中等)
题目:
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
示例 2:
示例 3:
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都 互不相同
1 <= target <= 500
思路:
这题的思路主要跟上面的区别,在于数组是可以重复使用。
class Solution {
List<List<Integer>> list=new ArrayList<List<Integer>>();
//删除回溯使用双端队列比较好
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 先进行排序
backtrace(candidates,target,0,0);
return list;
}
public void backtrace(int []candidates,int target,int sum,int startindex){
//如果sum大于target 优先返回
if(sum>target)return ;
//记得转换类型
if(sum==target){
list.add(new ArrayList<>(sonlist));
return;
}
//从startindex开始遍历,而且条件是小于数组长
for(int i=startindex;i<candidates.length;i++){
sonlist.addLast(candidates[i]);
sum=sum+candidates[i];
//这里的数组值是可以重复的,所以startindex是可以为i本身自已。
backtrace(candidates,target,sum,i);
//移除最新的节点,也要移除sum中那个数组值
sonlist.removeLast();
sum=sum-candidates[i];
}
}
}
40. 组合总和 II(中等)
题目:
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
示例 2:
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
思路:
这道题跟上面的思路是大同小异,差不多,区别在于不可重复使用,但是数组中有值一样,遍历出来的列表,可能会有重复的,类似这种.
解决上面的问题:把所有组合求出来,再用set或者map去重,这么做很容易超时!
所以要在搜索的过程中就去掉重复组合。
可以通过外加这一行代码,具体如下所示:
只要大于下标值,而且值和之前那个相等,既跳过该下标即可
if(i>startindex&&candidates[i]==candidates[i-1])continue;
完整代码如下所示:
class Solution {
List<List<Integer>> list =new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtrace(candidates,target,0,0);
return list;
}
public void backtrace(int []candidates,int target,int sum,int startindex){
if(sum>target)return;
if(sum==target){
list.add(new ArrayList<>(sonlist));
return ;
}
for(int i=startindex;i<candidates.length;i++){
if(i>startindex&&candidates[i]==candidates[i-1])continue;
sonlist.addLast(candidates[i]);
sum=sum+candidates[i];
backtrace(candidates,target,sum,i+1);
sonlist.removeLast();
sum=sum-candidates[i];
}
}
}
也可以通过一维的标记数组,标记其已经使用过该数字了
class Solution {
List<List<Integer>> list =new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
//定义一个一维标记数组
boolean [] flag=new boolean [candidates.length];
backtrace(candidates,target,0,0,flag);
return list;
}
public void backtrace(int []candidates,int target,int sum,int startindex,boolean [] flag){
if(sum>target)return;
if(sum==target){
list.add(new ArrayList<>(sonlist));
return ;
}
for(int i=startindex;i<candidates.length;i++){
//比较的是前一个数组,数组的boolean也是上一个值
if(i>0&&candidates[i]==candidates[i-1]&&flag[i-1]==false)continue;
sonlist.addLast(candidates[i]);
sum=sum+candidates[i];
//一开始初始化为true
flag[i]=true;
backtrace(candidates,target,sum,i+1,flag);
//回溯之后变为false,标记数组
flag[i]=false;
sonlist.removeLast();
sum=sum-candidates[i];
}
}
}
可配合如下进行理解:
(关于该图来源于 代码随想录的网址)
子集
78. 子集(中等)
题目:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
示例 2:
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同
思路:
class Solution {
List<List<Integer>> list=new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrace(nums,0);
return list;
}
public void backtrace(int []nums,int startindex){
//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」。
list.add(new ArrayList<>(sonlist));
//终止条件本身可不加
if(sonlist.size()>=nums.length)return ;
for(int i=startindex;i<nums.length;i++){
sonlist.addLast(nums[i]);
backtrace(nums,i+1);
sonlist.removeLast();
}
}
}
90. 子集 II(中等)
题目:
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
示例 2:
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
思路:
添加标记数组:
class Solution {
List<List<Integer>> list =new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
//先排好顺序
Arrays.sort(nums);
boolean []flag=new boolean[nums.length];
backtrace(nums,0,flag);
return list;
}
public void backtrace(int []nums,int startindex,boolean [] flag){
//该算法为找子集,每一次遍历都可以输出添加
list.add(new ArrayList<>(sonlist));
//开始的下标值如果大于字段长,就返回
if(startindex>=nums.length)return;
for(int i=startindex;i<nums.length;i++){
//通过大于下标以及如果两者相等,跳过该循环
//切记flag的下标也是i-1,而不是i,重点记忆
if(i>0&&nums[i]==nums[i-1]&&flag[i-1]==false)continue;
sonlist.addLast(nums[i]);
flag[i]=true;
backtrace(nums,i+1,flag);
flag[i]=false;
sonlist.removeLast();
}
}
}
不添加标记数组的方式:
class Solution {
List<List<Integer>> list =new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
//先排好顺序
Arrays.sort(nums);
backtrace(nums,0);
return list;
}
public void backtrace(int []nums,int startindex){
//该算法为找子集,每一次遍历都可以输出添加
list.add(new ArrayList<>(sonlist));
//开始的下标值如果大于字段长,就返回
if(startindex>=nums.length)return;
for(int i=startindex;i<nums.length;i++){
//通过大于下标以及如果两者相等,跳过该循环
if(i>startindex&&nums[i]==nums[i-1])continue;
sonlist.addLast(nums[i]);
backtrace(nums,i+1);
sonlist.removeLast();
}
}
}
排列
46. 全排列(中等)(不包含重复数字)
题目:
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
示例 2:
示例 3:
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同
思路:
这里和77.组合问题 、131.切割问题和78.子集问题
最大的不同就是for循环里不用startIndex了
因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。
排列问题总结
每层节点都是从0开始搜索而不是startindex
需要使用uded数组记录path放了哪些元素
class Solution {
List<List<Integer>> list=new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
boolean [] flag=new boolean[nums.length];
backtrace(nums,flag);
return list;
}
public void backtrace(int [] nums,boolean [] flag){
if(sonlist.size()==nums.length ){
list.add(new ArrayList<>(sonlist));
return ;
}
//每一层回溯遍历,都是从0开始,判断是否为true
// 1为true,在回溯的时候,1还是为true,则跳过1,来到了2。遍历完1 2 3类似这样子后。
for(int i=0;i<nums.length;i++){
//如果这一层用了true的节点,则跳回
if(flag[i]==true)continue;
sonlist.addLast(nums[i]);
flag[i]=true;
backtrace(nums,flag);
flag[i]=false;
sonlist.removeLast();
}
}
}
47. 全排列 II(中等)
题目:
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
示例 2:
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
思路:
class Solution {
List<List<Integer>> list=new ArrayList<List<Integer>>();
LinkedList<Integer> sonlist=new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean [] flag=new boolean[nums.length];
Arrays.sort(nums);
backtrace(nums,flag,0);
return list;
}
public void backtrace(int []nums,boolean [] flag,int startindex){
if(sonlist.size()==nums.length){
list.add(new ArrayList<>(sonlist));
return ;
}
for(int i=0;i<nums.length;i++){
if(i>0&&nums[i]==nums[i-1]&&flag[i-1]==false)continue;
if(flag[i]==true)continue;
sonlist.addLast(nums[i]);
flag[i]=true;
backtrace(nums,flag,startindex+1);
flag[i]=false;
sonlist.removeLast();
}
}
}
其他
491. 递增子序列(中等)
题目:
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
示例 2:
提示:
1 <= nums.length <= 15
-100 <= nums[i] <= 100
思路:
区别于上面其他思路:
因为原数组中不像之前的都是排序后在进行查重,所以不可使用之前的方法
在搜索遍历的时候就进行查重 而且 确定好排序
而且不可用上面思路中的标记数组
需要使用类似哈希表中的数组计数
注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希
class Solution {
List<List<Integer>> list =new ArrayList<List<Integer>>();
List<Integer> sonlist=new ArrayList<>();
boolean [] flag;
public List<List<Integer>> findSubsequences(int[] nums) {
flag=new boolean [nums.length];
backtrace(nums,0);
return list;
}
public void backtrace(int [] nums,int startindex){
//确定列表的尺寸大于等于2才添加进去
if(sonlist.size()>=2){
list.add(new ArrayList<>(sonlist));
}
if(startindex>nums.length)return ;
int[] used = new int[201];
for(int i=startindex;i<nums.length;i++){
//因为原数组中不像之前的都是排序后在进行查重,所以不可使用之前的方法
//在搜索遍历的时候就进行查重 而且 确定好排序
if(!sonlist.isEmpty()&&nums[i]<sonlist.get(sonlist.size()-1) ||used[nums[i] + 100] == 1 )continue;
used[nums[i] + 100] = 1;
flag[i]=true;
sonlist.add(nums[i]);
backtrace(nums,i+1);
sonlist.remove(sonlist.size() - 1);
}
}
}