回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
回溯法就从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。这个开始结点就成为一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为一个新的活结点,并成为当前扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止
题目:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
思路:
回溯算法的重点就是找到终止点和剪枝,此题每下个递归时target值为当前target值减去当前数组元素值,当target为0时表示找到结果集,返回。如果target < 0则表示之后的递归都不满足情况,直接返回,即剪枝。
注意:对于数组的回溯算法,需要先对数组进行排序,否则不能完成正确剪枝
代码:
class Solution {
public List> combinationSum(int[] c, int target) {
Arrays.sort(c);
List> res = new ArrayList<>();
if(c.length == 0) return res;
helper(c,0,target,new ArrayList(),res);
return res;
}
private void helper(int[] c,int start,int target,List list,List> res){
//剪枝
if(target < 0) return;
if(target == 0){
res.add(new ArrayList<>(list)); //注意这里插入的是List的镜像,否则只能保存它的引用,结果出错
}
for(int i = start;i < c.length;i++){
list.add(c[i]);
helper(c,i,target-c[i],list,res);
list.remove(list.size()-1);
}
}
}
题目:
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
思路:
这道题只需要在39题的基础上去重即可,即在数组排序后,比较当前元素和前一个元素是否相同,如果相同则continue
代码:
class Solution {
public List> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List> res = new ArrayList<>();
if(candidates.length <= 0) return res;
helper(candidates,target,0,new ArrayList(),res);
return res;
}
private void helper(int[] candidates,int target,int start,List list,List> res){
if(target < 0) return;
if(target == 0){
res.add(new ArrayList<>(list));
return;
}
for(int i = start;i < candidates.length;i++){
if(i != start && candidates[i] == candidates[i-1]) continue; //去重
list.add(candidates[i]);
helper(candidates,target-candidates[i],i+1,list,res);
list.remove(list.size()-1);
}
}
}
题目
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
思路
创建一个标识数组,和元素数组等长,用来记录当前索引的元素是否被使用,被使用赋值为1,没有使用为0。最外层循环固定结果的第一个元素,后面的元素由递归得到,每次递归都数组开头选取元素,如果当前元素已经使用,则选取下一个元素,直到第一个元素为数组最后一个值。这里递归的终止条件是集合中的元素长度等于数组的长度。
注意:数组需要先排序。
代码
class Solution {
public List> permute(int[] nums) {
Arrays.sort(nums);
List> res = new ArrayList<>();
if(nums.length == 0) return res;
//维护一个等长数组,用来判断当前元素是否访问过
int[] flag = new int[nums.length];
helper(nums,new ArrayList(),res,flag);
return res;
}
private void helper(int[] nums,List list,List> res,int[] flag){
if(list.size() == nums.length){
//插入镜像
res.add(new ArrayList<>(list));
return;
}
for(int i = 0;i < nums.length;i++){
if(flag[i] == 0){
flag[i] = 1;
list.add(nums[i]);
helper(nums,list,res,flag);
list.remove(list.size()-1);
flag[i] = 0;
}
}
}
}
题目
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
思路
在46题的基础上去重即可,这里去重的方法是设定一个变量储存上一个访问元素,如果当前元素和上个访问元素相等,则continue,也可以使用40题的去重方法。当然这些都是建立在数组有序的基础上的。
代码
class Solution {
List> res = new ArrayList<>();
public List> permuteUnique(int[] nums) {
Arrays.sort(nums);
if(nums.length == 0) return res;
int[] flag = new int[nums.length];
helper(nums,flag,new ArrayList());
return res;
}
private void helper(int[] nums,int[] flag,List list){
if(list.size() == nums.length){
res.add(new ArrayList<>(list));
return;
}
//用来保存上一次访问的元素
int lastUsed = Integer.MIN_VALUE;
for(int i = 0;i < nums.length;i++){
//判断当前数组元素是否和上一个相同
if(flag[i] == 0 && nums[i] != lastUsed){
flag[i] = 1;
list.add(nums[i]);
helper(nums,flag,list);
lastUsed = nums[i];
list.remove(list.size()-1);
flag[i] = 0;
}
}
}
}
题目
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
思路
每次递归取下一个元素,且每次递归都将集合加入结果集,直到集合长度等于数组长度,剪枝。
代码
class Solution {
public List> subsets(int[] nums) {
Arrays.sort(nums);
List> res = new ArrayList<>();
if(nums.length == 0) return res;
helper(nums,0,new ArrayList<>(),res);
return res;
}
private void helper(int[] nums,int start,List list,List> res){
if(list.size() == nums.length){
res.add(new ArrayList<>(list));
return;
}
res.add(new ArrayList<>(list));
for(int i = start;i < nums.length;i++){
list.add(nums[i]);
helper(nums,i+1,list,res);
list.remove(list.size()-1);
}
}
}
题目
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
思路
思路和78一致,去重即可,采用上述题目的两种去重方法都可以
代码
class Solution {
public List> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
List> res = new ArrayList<>();
if(nums.length == 0) return res;
helper(nums,0,new ArrayList<>(),res);
return res;
}
private void helper(int[] nums,int start,List list,List> res){
res.add(new ArrayList<>(list));
for(int i = start;i < nums.length;i++){
if(i > start && nums[i] == nums[i-1])
continue;
list.add(nums[i]);
helper(nums,i+1,list,res);
list.remove(list.size()-1);
}
}
}
题目
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
思路:
比起上面几题,这道题添加了一个限定因素,即元素出现个数,在递归时加入k值控制即可
代码:
class Solution {
public List> combinationSum3(int k, int n) {
List> res = new ArrayList<>();
helper(1,n,k,new ArrayList<>(),res);
return res;
}
private void helper(int start,int target,int count,List list,List> res){
if(target == 0 && count == 0){
res.add(new ArrayList<>(list));
return;
}
//剪枝,减少不必要的循环
if(target <= 0 || count <= 0) return;
for(int i = start;i <= 9;i++){
list.add(i);
helper(i+1,target-i,count-1,list,res);
list.remove(list.size()-1);
}
}
}