参考 https://blog.csdn.net/wonner_/article/details/80373871
回溯算法的定义:回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。适用于求解组合数较大的问题。
对于回溯问题,总结出一个递归函数模板,包括以下三点
递归函数的开头写好跳出条件,满足条件才将当前结果加入总结果中
已经拿过的数不再拿 if(s.contains(num)){continue;}
遍历过当前节点后,为了回溯到上一步,要去掉已经加入到结果list中的当前节点。
针对不同的题目采用不同的跳出条件或判断条件
Given a collection of distinct integers, return all possible permutations.
这是个排列问题,排列问题,需要每次都分裂所有的原数组(index 从0 开始),但又要去重,对于46因为没有重复元素,可以直接contains 去判断。
画出递归树如下:每次都需要从 index 为0 分裂, 为了去重 可以设置一个 boolean[] flag来标记已经访问的,如果没有重复元素可以直接contains判断重复,图中红色部分为需要去重的数。
class Solution { public List<List<Integer>> permute(int[] nums) { List<List<Integer>> result = new ArrayList<>(); backTracking(new ArrayList<>(), result,nums); return result; } void backTracking(List<Integer> curResult, List<List<Integer>> result, int[] nums){ if(curResult.size() == nums.length){ //System.out.println(curResult); result.add(new ArrayList<>(curResult)); return; } for(int i=0; i<nums.length; i++){ if(!curResult.contains(nums[i])){ //给出的条件是数组里数字都是distinct 才可以这么判断 curResult.add(nums[i]); backTracking(curResult, result,nums); //System.out.println(curResult); curResult.remove(curResult.size()-1); } } } }
以上算法复杂度, 每次都是N, 而且判断curResult.contains 时也是和长度有关, 本质上应该是 N*N*N... = N^N, 甚至更高
为了不用每次去判断 contains ,可以用一个 boolean used[nums.length] 去重,以空间换时间
class Solution { public List<List<Integer>> permute(int[] nums) { List<List<Integer>> result = new ArrayList<>(); backTracking(new ArrayList<>(), result,nums,new boolean[nums.length]); return result; } void backTracking(List<Integer> curResult, List<List<Integer>> result, int[] nums,boolean[] used){ if(curResult.size() == nums.length){ //System.out.println(" Result: "+ curResult); result.add(new ArrayList<>(curResult)); return; } for(int i=0; i<nums.length; i++){ if(!used[i]){ curResult.add(nums[i]); used[i] = true; backTracking(curResult, result,nums,used); used[i] = false; curResult.remove(curResult.size()-1); } } } }
以下算法只需要了解即可:
别人优化的算法: http://www.noteanddata.com/classic-algorithm-coding-backtrack-permutation.html
每次添加一个数时,不需要从0 开始for 循环遍历,但是需要一个swap 去交换, 改进的算法code 如下:
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> allList = new ArrayList<>();
helper(nums, 0, new ArrayList<>(), allList);
return allList;
}
public void helper(int[] nums, int from, List<Integer> cur, List<List<Integer>> allList) {
if(cur.size() == nums.length) {
allList.add(new ArrayList<Integer>(cur));
return;
}
for(int i = from; i < nums.length; ++i) {
swap(nums, from, i);
cur.add(nums[from]);
helper(nums, from+1, cur, allList);
cur.remove(cur.size()-1);
swap(nums, from, i);
}
}
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
这个算法不是太好理解,画了一个递归的分解图如下: 每次 from 和 i 都需要交换, 把需要放的value 放在from 的位置上。不考虑交换成本,该算法 是N* N-1*N-2. ...*1 = N!
推广: 如果从1~n 个元素中取K 个全排列,code 如下
public List<List<Integer>> permutation(int n, int k) { int[] arr = new int[n]; for(int i = 0; i < arr.length; ++i) { arr[i] = i+1; } List<List<Integer>> allList = new ArrayList<>(); dfs(arr, 0, k, new ArrayList<Integer>(), allList); return allList; } public void dfs(int[] arr, int from, int k, List<Integer> cur, List<List<Integer>> allList) { if(cur.size() == k) { allList.add(new ArrayList<>(cur)); return; } for(int i = from; i < arr.length; ++i) { swap(arr, from, i); cur.add(arr[from]); dfs(arr, from+1, k, cur, allList); cur.remove(cur.size()-1); swap(arr, from, i); } } void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
47. 如果数组元素有重复元素,求组合。
1. 如果有重复,通过 contains 判断是否已经放了该数字,不适用, 只能通过 boolean used 来标记是否访问过。
2. 对于重复数字判定, 得先排序, 这个条件 不符合 if(used[i] || i>0 && nums[i]==nums[i-1] && !used[i-1]) continue; 不是这个条件的则放数
为何 这里判断条件是 num[i]==num[i-1] && !used[i-1] , 这里下标是i-1 而不是i, 从下面这个dfs tree 红色圈中 可以看出,虽然是重复元素,但如果同一层左边没放用,是可以用 后面重复的。
上面这段理解是我第一次的理解,等我第二次review 时我发现是错误的,是否选择 一个数字的原则 当前 i , 如果和 i-1 重复, 那么选择i 是因为 选择了 i-1, 如果没选择 i-1 就不要再选择 i了,这才是正确的理解。
// class Solution { // public List<List<Integer>> permuteUnique(int[] nums) { // List<List<Integer>> result = new ArrayList<>(); // backTracking(new ArrayList<>(), new HashSet<>(), result,nums); // return result; // } // void backTracking(List<Integer> curResult, Set<Integer> set, List<List<Integer>> result, int[] nums){ // if(curResult.size() == nums.length){ // //System.out.println(curResult); // result.add(new ArrayList<>(curResult)); // return; // } // for(int i=0; i<nums.length; i++){ // if(!set.contains(i)){ // curResult.add(nums[i]); // set.add(i); // backTracking(curResult, set, result,nums); // //System.out.println(curResult); // curResult.remove(curResult.size()-1); // set.remove(i); // } // } // } // } class Solution { public List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(nums); backTracking(new ArrayList<>(), result,nums,new boolean[nums.length]); return result; } void backTracking(List<Integer> curResult, List<List<Integer>> result, int[] nums,boolean[] used){ if(curResult.size() == nums.length){ result.add(new ArrayList<>(curResult)); return; } for(int i=0; i<nums.length; i++){ if(used[i] || i>0 && nums[i]==nums[i-1] && !used[i-1]) continue; { curResult.add(nums[i]); used[i] = true; backTracking(curResult,result,nums,used); curResult.remove(curResult.size()-1); used[i] = false; } } } }