回溯算法
1.回溯算法与DFS的区别
DFS是朝某一个方向搜索,而回溯算法建立在DFS之上,在回溯中,达到结束条件后,恢复状态,回复上一层,再次搜索。因此回溯和DFS的区别在于有无状态重置(恢复状态)
2.使用回溯算法的时机
一般遇到子集、排列组合、搜索类问题时需要用到
子集与组合问题
①子集
给你一个整数数组 nums ,数组中的元素互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集
public List<List<Integer>> subsets(int[] nums) {
int len=nums.length;
List<List<Integer>> res=new ArrayList<>();
bachTrack(nums,0,new ArrayList<>(),res);
return res;
}
public void bachTrack(int[] nums,int start,ArrayList<Integer> temp,List<List<Integer>> res){
res.add(new ArrayList<>(temp));
for(int i=start;i<nums.length;i++){
temp.add(nums[i]);
//递归进入下一层,此时从i+1开始遍历
bachTrack(nums,i+1,temp,res);
temp.remove(temp.size()-1);
}
}
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res=new ArrayList<>();
backTrack(nums,0,res,new ArrayList<>());
return res;
}
public void backTrack(int[] nums,int index,List<List<Integer>> res,List<Integer> temp){
res.add(new ArrayList<>(temp));
for(int i=index;i<nums.length;i++){
经过排序后,相同的元素相邻,我们只用其中一个元素即可,在此进行剪枝去重
if(i>index&&nums[i]==nums[i-1])
continue;
temp.add(nums[i]);
backTrack(nums,i+1,res,temp);
temp.remove(temp.size()-1);
}
③组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res=new ArrayList<>();
backTrack(n,k,1,res,new ArrayList<>());
return res;
}
public void backTrack(int n,int k,int start,List<List<Integer>> res,List<Integer> temp){
当组合够k个元素时,就打破递归
if(temp.size()==k){
res.add(new ArrayList<>(temp));
return;
}
for(int i=start;i<=n;i++){
temp.add(i);
backTrack(n,k,i+1,res,temp);
temp.remove(temp.size()-1);
}
}
④组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
Arrays.sort(candidates);
backTrack(candidates,target,0,0,res,new ArrayList<>());
return res;
}
public void backTrack(int[] candidates,int target,int sum,int start,List<List<Integer>> res,ArrayList<Integer> temp){
//当sum==target就打破循环
if(sum==target){
res.add(new ArrayList<>(temp));
return;
}
for(int i=start;i<candidates.length;i++){
//注意此处要开辟一个新的变量来接收sum,否则sum数据会混乱
int rs=candidates[i]+sum;
if(rs<=target){
temp.add(candidates[i]);
//因为每个元素可以被重复使用,所以递归依旧从i开始
backTrack(candidates,target,rs,i,res,temp);
temp.remove(temp.size()-1);
}else
break;
}
}
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
private List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backTrack(candidates,0,target,0,new ArrayList<>());
return res;
}
public void backTrack(int[] candidates,int start,int target,int sum,ArrayList<Integer> temp){
if(sum==target){
res.add(new ArrayList<>(temp));
return;
}
for(int i=start;i<candidates.length;i++){
//排序去重,相同的元素使用一次
if(i>start&&candidates[i]==candidates[i-1]){
continue;
}
int rs=candidates[i]+sum;
if(rs<=target){
temp.add(candidates[i]);
//递归从i+1开始
backTrack(candidates,i+1,target,rs,temp);
temp.remove(temp.size()-1);
}
}
}
全排列问题
注意区分排列和组合的区别,排列区分数据的先后顺序,组合不区分顺序
①全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
//数组用来标定该数据是否被访问过了
int[] visited = new int[nums.length];
backtrack(res, nums, new ArrayList<Integer>(), visited);
return res;
}
private void backtrack(List<List<Integer>> res, int[] nums, ArrayList<Integer> tmp, int[] visited) {
//因为是全排列,只有当list够全排列的长度才会保存
if (tmp.size() == nums.length) {
res.add(new ArrayList<>(tmp));
return;
}
for (int i = 0; i < nums.length; i++) {
//如果数据已经被访问过,则跳过
if (visited[i] == 1) continue;
visited[i] = 1;
tmp.add(nums[i]);
backtrack(res, nums, tmp, visited);
//将标志位恢复
visited[i] = 0;
tmp.remove(tmp.size() - 1);
}
}
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列
private List<List<Integer>> res=new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
int[] flag=new int[nums.length];
backTrack(nums,flag,new ArrayList<Integer>());
return res;
}
public void backTrack(int[]nums,int[] flag,ArrayList<Integer> temp){
if(temp.size()==nums.length){
res.add(new ArrayList<>(temp));
return;
}
for(int i=0;i<nums.length;i++){
//剪枝问题,排序去重
if(i>0&&nums[i]==nums[i-1]&&flag[i-1]==0)
continue;
if(flag[i]==1)
continue;
flag[i]=1;
temp.add(nums[i]);
backTrack(nums,flag,temp);
flag[i]=0;
temp.remove(temp.size()-1);
}
}
总结:
1.子集
当给定一个数组,其中元素互不相同,返回所有不重复的子集用以下方法
public void bachTrack(int[] nums,int start,ArrayList<Integer> temp,List<List<Integer>> res){
res.add(new ArrayList<>(temp));
for(int i=start;i<nums.length;i++){
temp.add(nums[i]);
bachTrack(nums,i+1,temp,res);
temp.remove(temp.size()-1);
}
}
当其中元素有重复时,先将数组排序,加上剪枝的算法
public void backTrack(int[] nums,int index,List<List<Integer>> res,List<Integer> temp){
res.add(new ArrayList<>(temp));
for(int i=index;i<nums.length;i++){
//剪枝算法
if(i>index&&nums[i]==nums[i-1])
continue;
temp.add(nums[i]);
backTrack(nums,i+1,res,temp);
temp.remove(temp.size()-1);
}
}
2.组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。只需判定temp.size()==k即可返回
public void backTrack(int n,int k,int start,List<List<Integer>> res,List<Integer> temp){
if(temp.size()==k){
res.add(new ArrayList<>(temp));
return;
}
for(int i=start;i<=n;i++){
temp.add(i);
backTrack(n,k,i+1,res,temp);
temp.remove(temp.size()-1);
}
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字只可以使用一次。依旧排序加剪枝
public void backTrack(int[] candidates,int start,int target,int sum,ArrayList<Integer> temp){
if(sum==target){
res.add(new ArrayList<>(temp));
return;
}
for(int i=start;i<candidates.length;i++){
if(i>start&&candidates[i]==candidates[i-1])
continue;
int rs=candidates[i]+sum;
if(rs<=target){
temp.add(candidates[i]);
backTrack(candidates,i+1,target,rs,temp);
temp.remove(temp.size()-1);
}
}
}
当candidates 中的数字可以无限制重复被选取,此时start每次递归都需从i开始,而不是i+1
public void backTrack(int[] candidates,int target,int sum,int start,List<List<Integer>> res,ArrayList<Integer> temp){
if(sum==target){
res.add(new ArrayList<>(temp));
return;
}
for(int i=start;i<candidates.length;i++){
int rs=candidates[i]+sum;
if(rs<=target){
temp.add(candidates[i]);
backTrack(candidates,target,rs,i,res,temp);
temp.remove(temp.size()-1);
}else
break;
}
}
3.排列
排列和组合不同,元素顺序改变则变成新的排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。设定一个int[] flag来标定是否被访问,如被访问则跳过
private void backtrack(List<List<Integer>> res, int[] nums, ArrayList<Integer> tmp, int[] visited) {
if (tmp.size() == nums.length) {
res.add(new ArrayList<>(tmp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (visited[i] == 1) continue;
visited[i] = 1;
tmp.add(nums[i]);
backtrack(res, nums, tmp, visited);
visited[i] = 0;
tmp.remove(tmp.size() - 1);
}
}
当给定的数组包含重复元素时,需考虑剪枝问题
public void backTrack(int[]nums,int[] flag,ArrayList<Integer> temp){
if(temp.size()==nums.length){
res.add(new ArrayList<>(temp));
return;
}
for(int i=0;i<nums.length;i++){
//
if(i>0&&nums[i]==nums[i-1]&&flag[i-1]==0)
continue;
if(flag[i]==1)
continue;
flag[i]=1;
temp.add(nums[i]);
backTrack(nums,flag,temp);
flag[i]=0;
temp.remove(temp.size()-1);
}
}
求子集问题:当元素互不相同则用最简单的回溯算法。当有相同元素,先排序,再用if(i>index&&nums[i]==nums[i-1])continue;判断剪枝
组合问题:先找到递归终止的条件,然后使用回溯算法,如果元素可以无限制重复使用,则递归从i开始,否则从i+1开始
排列问题:当元素都不重复时,先找到递归终止的条件,比如temp.sizr()==nums.length,使用int[] flag标记元素是否被使用,当回溯时,flag的状态也要重置。有重复元素时,排序加剪枝算法if(i>0&&nums[i]==nums[i-1]&&flag[i-1]==0)continue;
17. 电话号码的字母组合
采用回溯法,注意map多组数据的初始化方法
public List<String> letterCombinations(String digits) {
List<String> list = new ArrayList<>();
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");
}};
backTrack(list, 0, new StringBuilder(), map, digits);
return list;
}
public void backTrack(List<String> list, int index, StringBuilder sb, Map<Character, String> map, String digits) {
if (sb.length() == digits.length()) {
list.add(sb.toString());
return;
}
char c = digits.charAt(index);
String s = map.get(c);
for (int i = 0; i < s.length(); i++) {
sb.append(s.charAt(i));
backTrack(list, index + 1, sb, map, digits);
sb.deleteCharAt(sb.length() - 1);
}
}