回溯法写法思路:
39. 组合总和
private List<List<Integer>> res;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
res = new ArrayList<>();
if (candidates == null || candidates.length == 0 || target < 0) return res;
Arrays.sort(candidates);//排序可以提前终止判断排序的主要作用是剪枝
generateCombination(candidates,0, target, new ArrayList<>());
return res;
}
private void generateCombination(int[] candidates,int start, int target, ArrayList<Integer> temp) {
if (target == 0){
res.add(new ArrayList<>(temp)); //Java中的可变对象时引用传递,需要将当前路径里面的值拷贝出来
return;
}
//在循环的时候做判断,target - candidates[i] 表示下一轮的剩余 ,如果下一轮的剩余都小于0了,就没有必要在进行后面的循环了。
//这一点基于数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小。
for (int i = start; i < candidates.length && target - candidates[i] >= 0; i++) {
temp.add(candidates[i]);
//因为元素可以重复使用,这里递归下去的是i,不是i + 1
generateCombination(candidates, i, target - candidates[i],temp);
//java的引用传递,导致list数组一直增多,在每次递归完成之后,要进行一次回溯。把最新加入的那个数删除。
temp.remove(temp.size() - 1);
}
}
40. 组合总和 II
List<List<Integer>> res;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
res = new ArrayList<>();
if (candidates == null || candidates.length == 0 || target < 0)return res;
Arrays.sort(candidates);//排序可以提前终止判断
generateCombination(candidates, 0 , target, new ArrayList<Integer>());
return res;
}
private void generateCombination(int[] candidates, int start, int target, ArrayList<Integer> temp) {
if (target == 0) {
res.add(new ArrayList<>(temp));
return;
}
for (int i = start; i < candidates.length && target - candidates[i] >= 0; i++) {
//因为这个数和上个数相同,所以从这个数开始的情况,在上个数里面考虑过了,需要跳过(去重)
if(i > start && candidates[i] == candidates[i - 1])continue;
temp.add(candidates[i]);
//元素不可以重复,这里递归传递下去的是i + 1,而不是i
generateCombination(candidates, i + 1, target - candidates[i], temp);
temp.remove(temp.size() - 1);
}
}
216. 组合总和 III
1、尝试做减法,减到 0 就说明可能找到了一个符合题意的组合,但是题目对组合里元素的个数有限制,因此还需要对元素个数做判断;
2、如果减到负数,也没有必要继续搜索下去;
3、因为结果集里的元素互不相同,因此下一层搜索的起点应该是上一层搜索的起点值 + 1;
List<List<Integer>> res;
public List<List<Integer>> combinationSum3(int k, int target) {
res = new ArrayList<>();
if (target < 0 && k <= 0)return res;
generateCombination(k , target , 1, new ArrayList<Integer>());
return res;
}
private void generateCombination(int k, int target, int start, ArrayList<Integer> temp) {
if (target == 0 && temp.size() == k) {
res.add(new ArrayList<>(temp)); //对象类型的状态变量,在方法传递中是引用传递,在结算的时候需要做一个拷贝
return;
}
// temp.size() <= k 剪枝
// target - i >= 0 剪枝
for (int i = start; i <= 9 && temp.size() <= k && target - i >= 0; i++) {
temp.add(i);
generateCombination(k, target - i, i + 1, temp);
temp.remove(temp.size() - 1); //对象类型的状态变量,在方法传递中是引用传递,在回溯的时候,需要重置线程
}
}
377. 组合总和 Ⅳ
回溯 超时 每次递归都是从0开始的,所有的数字都遍历一遍,就可以出现重复的组合
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
public int combinationSum4(int[] nums, int target) {
if (nums == null || nums.length == 0 || target < 0){
return 0;
}
Arrays.sort(nums);
generationCombinationSum(nums, target, new ArrayList<>());
return res.size();
}
public void generationCombinationSum(int[] nums, int target, ArrayList<Integer> temp) {
if (target == 0){
res.add(new ArrayList(temp));
return;
}
for (int i = 0; i < nums.length && target - nums[i] >= 0; i++) {
temp.add(nums[i]);
generationCombinationSum(nums, target - nums[i], temp);
temp.remove(temp.size() - 1);
}
return;
}
重叠子问题,可以使用动态规划来做,如果要求具体的解,可以用“回溯算法”
//不用得到具体的组合表示,可以用动态规划
//dp[i] : 数组中和为i的组合的个数
//dp[i] = dp[i - nums[0]] + dp[i - nums[1]] + dp[i - nums[2]]
//[1, 3, 4] dp[7] = dp[6] + dp[4] + dp[3]
public int combinationSum4(int[] nums, int target) {
if (nums.length == 0)return 0;
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int j = 0; j < nums.length; j++) {
if (i - nums[j] >= 0) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
77. 组合
剪枝
当选定了一个元素,即 pre.size() == 1 的时候,接下来要选择 2 个元素, i 最大的值是 4 ,因为从 5 开始选择,就无解了;
当选定了两个元素,即 pre.size() == 2 的时候,接下来要选择 1 个元素, i 最大的值是 5 ,因为从 6 开始选择,就无解了。
i <= n - (k - temp.size()) + 1
List<List<Integer>> res;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList<>();
if (n <= 0 || k <= 0 || k > n)return res;
generationCombine(n, 1 , k, new ArrayList<>());
return res;
}
private void generationCombine(int n, int start, int k, ArrayList<Integer> temp) {
if (temp.size() == k) {
res.add(new ArrayList<>(temp));
return;
}
for (int i = start; i <= n - (k - temp.size()) + 1; i++) {
temp.add(i);
generationCombine(n,i + 1,k, temp);
temp.remove(temp.size() - 1);
}
}
46. 全排列
题解回溯算法详解
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表
List<List<Integer>> res;
public List<List<Integer>> permute(int[] nums) {
res = new ArrayList<>();
if (nums == null || nums.length == 0)return res;
boolean[] used = new boolean[nums.length];
generatePermutation(nums, used, new ArrayList<Integer>());
return res;
}
private void generatePermutation(int[] nums, boolean[] used, ArrayList<Integer> temp) {
if (temp.size() == nums.length) {
res.add(new ArrayList<>(temp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
temp.add(nums[i]);
generatePermutation(nums, used, temp);
used[i] = false;
temp.remove(temp.size() - 1);
}
}
和之前的元素不一样,全排列要求每个元素都必须出现一次,所以需要boolean数组来标记是否访问过了。而且每次循环都是从 i = 0开始
全排列 II
List<List<Integer>> res;
public List<List<Integer>> permuteUnique(int[] nums) {
res = new ArrayList<>();
if (nums == null || nums.length == 0) return res;
Arrays.sort(nums);//排序的目的是为了判断重复数字是否选过
boolean[] used = new boolean[nums.length];
generatePermutation(nums, used, new ArrayList<Integer>());
return res;
}
private void generatePermutation(int[] nums, boolean[] used, ArrayList<Integer> temp) {
if (temp.size() == nums.length) {
res.add(new ArrayList<>(temp));
return;
}
//递归拆解,从选择列表中选择何时条件的数字
for (int i = 0; i < nums.length; i++) {
//为了避免重复,只选择第一个重复的数字进行dfs
//如果当前的数和前一个数相等,判断前一个数是否选择过,如果选择过则跳过,正面前面已经选过重复的数字了
if (used[i] ||(i > 0 && nums[i] == nums[i - 1] && used[i - 1])) {
continue;
}
used[i] = true;// 将该选择从选择列表移除
temp.add(nums[i]);//路径.add(选择)
generatePermutation(nums, used, temp);
//回溯
used[i] = false;//将该选择再加入选择列表
temp.remove(temp.size() - 1);// 路径.remove(选择)
}
}
78. 子集
List<List<Integer>> res;
public List<List<Integer>> subsets(int[] nums) {
res = new ArrayList<>();
if (nums == null || nums.length == 0)return res;
generateSubsets(nums, 0 , new ArrayList<Integer>());
return res;
}
private void generateSubsets(int[] nums, int start,ArrayList<Integer> temp) {
res.add(new ArrayList<>(temp));
for (int i = start; i < nums.length; i++) {
temp.add(nums[i]);
generateSubsets(nums, i + 1, temp);
temp.remove(temp.size() - 1);
}
}
90. 子集 II
同样是包含重复元素
重复元素先排序,然后在选择列表的时候记得去重
List<List<Integer>> res;
public List<List<Integer>> subsets(int[] nums) {
res = new ArrayList<>();
if (nums == null || nums.length == 0)return res;
Arrays.sort(nums);
generateSubsets(nums, 0 , new ArrayList<Integer>());
return res;
}
private void generateSubsets(int[] nums, int start, ArrayList<Integer> temp) {
res.add(new ArrayList<>(temp));
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i] == nums[i - 1])continue;
temp.add(nums[i]);
generateSubsets(nums, i + 1, temp);
temp.remove(temp.size() - 1);
}
}
17. 电话号码的字母组合
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0)return res;
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
generateCombinations(digits,0,"");
return res;
}
private void generateCombinations(String digits, int start, String temp) {
if (start == digits.length()){
res.add(temp);
return;
}
Character c = digits.charAt(start);
String letter = map.get(c);
for (int i = 0; i < letter.length(); i++) {
generateCombinations(digits, start + 1,temp + letter.charAt(i));
}
}