回溯算法理论基础
1、什么是回溯法
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
回溯是递归的副产品,只要有递归就会有回溯。
回溯函数也就是递归函数,指的都是一个函数。
2、回溯法的效率
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
3、回溯法解决的问题
回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
4、回溯的模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
1、77. 组合
题意:给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
思路:回溯法
- 递归函数返回值和参数,返回值为空,参数传入n和k,以及每次递归开始位置startIndex,存储过程的集合path、存储结果的集合result
void combine(int n, int k, int startIndex, List<Integer> path, List<List<Integer>> result)
- 确定终止条件,当过程集合path的大小等于k时,就可以终止,并将过程集合存储到结果集合中
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
- 确定单次递归的条件
- for循环条件,i从startIndex开始,当i小于等于n时停止
- 循环体中,首先将当前i存储到过程集合中,然后递归下一行,递归结束后移除过程集合中的值
- 剪枝优化:
- 剪枝优化主要在for循环的终止条件上,比如说4,4,第一层只能等于1,再往后遍历没有意义。
- 因此可以将for循环的终止条件修改为
i <= n - (k - path.size()) + 1
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
combine(n, k, startIndex + 1, path, result);
path.remove(path.size() - 1);
}
整体代码如下:
package com.yzu.lee.recall;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: Combine
* @Description:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
* @author: Leekuangyew
* @date: 2022/6/2 20:13
*/
public class Combine {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
combine(n, k, 1, path, result);
return result;
}
private void combine(int n, int k, int startIndex, List<Integer> path, List<List<Integer>> result) {
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
combine(n, k, startIndex + 1, path, result);
path.remove(path.size() - 1);
}
}
}
2、216.组合总和III
题意:找出所有相加之和为 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和n,传入count用来记录当前值是否已经满足和为n的要求,startIndex为每次递归开始的条件,路径集合path和结果集合result
void combinationSum3(int k, int n, int count, int startIndex, List<Integer> path, List<List<Integer>> result)
- 确定递归结束的条件,如果当前路径集合的大小等于k,则执行判断count是否等于0,如果等于0,则将路径集合传入到结果集合中,最终返回
if (path.size() == k) {
if (count == 0) {
result.add(new ArrayList<>(path));
}
return;
}
- 确定单次递归逻辑,当i小于等于9时,一直循环遍历递归
for (int i = startIndex; i <= 9; i++) {
path.add(i);
count -= i;
combinationSum3(k, n, count, i + 1, path, result);
path.remove(path.size() - 1);
count += i;
}
- 剪枝优化,主要有两处,一处为循环结束条件,当
i <= 9 - (k - path.size()) + 1
时就结束循环,第二处在循环体中判断count-i如果小于0,则该数层不再继续往后遍历,也不再加树枝
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
if(count - i < 0) break;
path.add(i);
count -= i;
System.out.println(count);
combinationSum3(k, n, count, i + 1, path, result);
path.remove(path.size() - 1);
count += i;
}
17.电话号码的字母组合
题意:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例: 输入:“23” 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
思路:回溯法
- 数字与字母的映射关系
String[] strings = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
- 确定递归函数的返回值和参数,参数传过来按键数字字符串,按键数字与字符串的映射关系数组,以及num用于记录已经统计了几个路径
void letterCombinations(String digits, String[] strings, int num)
- 确定终止条件,如果按键数字字符串的长度与num相等时,将字符串存入结果集合中
if (digits.length() == num) {
result.add(sb.toString());
return;
}
- 确定单次递归的逻辑,首先根据题目的按键字符串在num位置的字符-’0‘,根据这个值作为索引,去找到映射数组中对应的字符串,然后遍历字符串,递归时需要将num+1;
String string = strings[digits.charAt(num) - '0'];
for (int i = 0; i < string.length(); i++) {
sb.append(string.charAt(i));
letterCombinations(digits, strings, num + 1);
sb.deleteCharAt(sb.length() - 1);
}
整体代码如下:
package com.yzu.lee.recall;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: LetterCombinations
* @Description:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
* 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
* @author: Leekuangyew
* @date: 2022/6/5 15:05
*/
public class LetterCombinations {
List<String> result = new ArrayList<>();
StringBuilder sb = new StringBuilder();
public List<String> letterCombinations(String digits) {
if (digits.length() == 0) return result;
String[] strings = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
letterCombinations(digits, strings, 0);
return result;
}
private void letterCombinations(String digits, String[] strings, int num) {
if (digits.length() == num) {
result.add(sb.toString());
return;
}
String string = strings[digits.charAt(num) - '0'];
for (int i = 0; i < string.length(); i++) {
sb.append(string.charAt(i));
letterCombinations(digits, strings, num + 1);
sb.deleteCharAt(sb.length() - 1);
}
}
}
39. 组合总和
题意:给定一个无重复元素的数组 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] ]
思路:回溯法
- 确定递归函数返回值和参数,参数传入candidates和target,传入中间集合list和结果集合result,起始位置startIndex
void combinationSum(int[] candidates, int target, List<Integer> list, List<List<Integer>> result, int startIndex)
- 确定终止条件,当target等于0时,将中间集合存入到结果集合中,并直接返回
if (target == 0) {
result.add(new ArrayList<>(list));
return;
}
- 确定单次递归的逻辑,由于同一个元素可以重复使用,所以递归时还是传入i,剪枝优化首先对数组进行排序,然后当
taeget - candidates[i] < 0
时,直接break,即该树层不再继续往后遍历,也不再往下遍历
for (int i = startIndex; i < candidates.length ; i++) {
if(taeget - candidates[i] < 0) break;
target -= candidates[i];//剪枝优化
list.add(candidates[i]);
combinationSum(candidates, target, list, result, i);
list.remove(list.size() - 1);
target += candidates[i];
}
整体代码如下:
package com.yzu.lee.recall;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName: CombinationSum
* @Description:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
* candidates 中的数字可以无限制重复被选取。
* @author: Leekuangyew
* @date: 2022/6/6 15:55
*/
public class CombinationSum {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
Arrays.sort(candidates);//剪枝优化
combinationSum(candidates, target, list, result, 0);
return result;
}
private void combinationSum(int[] candidates, int target, List<Integer> list, List<List<Integer>> result, int startIndex) {
if (target < 0) return;
if (target == 0) {
result.add(new ArrayList<>(list));
return;
}
for (int i = startIndex; i < candidates.length ; i++) {
target -= candidates[i];//剪枝优化
list.add(candidates[i]);
combinationSum(candidates, target, list, result, i);
list.remove(list.size() - 1);
target += candidates[i];
}
}
}
40.组合总和II
题意:给定一个数组 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] ]
思路:回溯法
- 确定递归函数返回值和参数,参数传入candidates和target,每次递归开始位置startIndex,中间集合list和结果集合result
void combinationSum2(int[] candidates, int target, int startIndex, List<Integer> list, List<List<Integer>> result)
- 确定终止条件,如果target等于0,将中间集合添加到结果集合中
if (target == 0) {
result.add(new ArrayList<>(list));
return;
}
- 确定单次递归的逻辑
- 正常for循环,终止条件为
i < candidates.length
,i从startIndex开始 - 剪枝优化,如果
target - candidates[i] < 0
,直接break - 剪枝优化,如果
i > startIndex && candidates[i] == candidates[i - 1]
,直接continue - 接下来就是正常的递归回溯
- 正常for循环,终止条件为
for (int i = startIndex; i < candidates.length; i++) {
if (target - candidates[i] < 0) break;
if (i > startIndex && candidates[i] == candidates[i - 1]) continue;
target -= candidates[i];
list.add(candidates[i]);
combinationSum2(candidates, target, i + 1, list, result);
target += candidates[i];
list.remove(list.size() - 1);
}
整体代码如下:
package com.yzu.lee.recall;
import com.sun.crypto.provider.PBEWithMD5AndDESCipher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName: CombinationSum2
* @Description:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
* candidates 中的每个数字在每个组合中只能使用一次。
* @author: Leekuangyew
* @date: 2022/6/6 17:49
*/
public class CombinationSum2 {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
Arrays.sort(candidates);
combinationSum2(candidates, target, 0, list, result);
return result;
}
private void combinationSum2(int[] candidates, int target, int startIndex, List<Integer> list, List<List<Integer>> result) {
if (target < 0) return;
if (target == 0) {
result.add(new ArrayList<>(list));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
if (target - candidates[i] < 0) break;
if (i > startIndex && candidates[i] == candidates[i - 1]) continue;
target -= candidates[i];
list.add(candidates[i]);
combinationSum2(candidates, target, i + 1, list, result);
target += candidates[i];
list.remove(list.size() - 1);
}
}
}
重点::使用used数组进行剪枝操作
分割问题
131.分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]
思路:回溯法
- 确定递归函数返回值和参数,参数传入s,递归起始位置startIndex,中间集合list和结果集合result
void partition(String s, int startIndex, List<String> list, List<List<String>> result)
- 确定终止条件,如果起始位置大于等于字符串s的长度,就直接将中间集合添加到结果中
if (startIndex >= s.length()) {
result.add(new ArrayList<>(list));
return;
}
- 确定单次递归的逻辑
- 正常for循环,开始
i = startIndex
,循环结束条件i < s.length()
- 判断s的startIndex到i之间是否为回文字串,如果是,则将字符串截断存放到中间集合中,否则直接continue
- 接下来就是正常的递归
- 正常for循环,开始
for (int i = startIndex; i < s.length(); i++) {
if (isPalindrome(s, startIndex, i)) {
String substring = s.substring(startIndex, i + 1);
list.add(new String(substring));
} else continue;
System.out.println(list);
partition(s, i + 1, list, result);
list.remove(list.size() - 1);
}
- 判断是否为回文子串,利用双指针法进行判断
private boolean isPalindrome(String s, int startIndex, int endIndex) {
for (int i = startIndex, j = endIndex; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) return false;
}
return true;
}
整体代码如下:
package com.yzu.lee.recall;
import org.hamcrest.core.Is;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: Partition
* @Description:给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
* 返回 s 所有可能的分割方案。
* @author: Leekuangyew
* @date: 2022/6/6 18:16
*/
public class Partition {
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
List<String> list = new ArrayList<>();
partition(s, 0, list, result);
return result;
}
private void partition(String s, int startIndex, List<String> list, List<List<String>> result) {
if (startIndex >= s.length()) {
result.add(new ArrayList<>(list));
return;
}
for (int i = startIndex; i < s.length(); i++) {
if (isPalindrome(s, startIndex, i)) {
String substring = s.substring(startIndex, i + 1);
list.add(new String(substring));
} else continue;
System.out.println(list);
partition(s, i + 1, list, result);
list.remove(list.size() - 1);
}
}
private boolean isPalindrome(String s, int startIndex, int endIndex) {
for (int i = startIndex, j = endIndex; i < j; i++, j--) {
System.out.println(j);
if (s.charAt(i) != s.charAt(j)) return false;
}
return true;
}
}
93.复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效的 IP 地址。
示例 1:
- 输入:s = “25525511135”
- 输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
- 输入:s = “0000”
- 输出:[“0.0.0.0”]
思路:回溯法
- 确定递归参数的返回值和参数,参数传入s,以及递归起始位置startIndex,中间集合result和小数点数量pointNum
void backtracking(String s, int startIndex, List<String> result, int pointNum)
- 确定终止条件,当小数点数量等于3时,判断字符串s的startIndex到
s.length() - 1
之间是否合法,合法的话就添加到中间节点,并直接返回
if (pointNum == 3) {
if (isValid(s, startIndex, s.length() - 1)) {
result.add(s);
}
return;
}
- 确定单次递归的逻辑
- 正常循环递归
- 如果当前截取的一部分合法,就直接对s进行处理,截取0到i+1的部分并拼接小数点,和s剩下来的部分
- 递归时需要将i+2
- 回溯时记得删除小数点
- 如果不合法,就直接break
for (int i = startIndex; i < s.length(); i++) {
if (isValid(s, startIndex, i)) {
s = s.substring(0, i + 1) + "." + s.substring(i + 1);
System.out.println(s);
pointNum++;
backtracking(s, i + 2, result, pointNum);
pointNum--;
s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉逗点
} else break;
}
- 判断当前截取的字符串是否满足条件
- 如果startIndex大于endIndex,直接返回false
- 如果startIndex不等于endIndex并且startIndex位置的字符串等于0,直接返回false
startIndex, endIndex + 1
截取字符串- 如果这个ip范围在合法范围就直接返回true
private boolean isValid(String s, int startIndex, int endIndex) {
if (startIndex > endIndex) return false;
if (startIndex != endIndex && s.charAt(startIndex) == '0') return false;
String substring = s.substring(startIndex, endIndex + 1);
// int ip = Integer.parseInt(substring);
long ip = Long.parseLong(substring);
if (ip >= 0 && ip <= 255) return true;
else return false;
}
子集问题
78.子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
思路:回溯法
- 确定递归函数的返回值和参数,参数传入nums,递归起始位置startIndex,中间集合path和结果结合result
void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result)
- 确定终止条件,不需要终止条件,循环结束就可以
- 确定单次递归的逻辑
- 每次递归开始,首先将中间集合添加到结果集合中
- for循环中,正常递归回溯
result.add(new ArrayList<>(path));
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);
backtracking(nums, i + 1, path, result);
path.remove(path.size() - 1);
}
整体代码如下:
package com.yzu.lee.recall;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: Subsets
* @Description:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
* 说明:解集不能包含重复的子集。
* @author: Leekuangyew
* @date: 2022/6/6 21:39
*/
public class Subsets {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) return result;
List<Integer> path = new ArrayList<>();
backtracking(nums, 0, path, result);
return result;
}
private void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result) {
result.add(new ArrayList<>(path));
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);
backtracking(nums, i + 1, path, result);
path.remove(path.size() - 1);
}
}
}
90.子集II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
- 输入: [1,2,2]
- 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
思路:回溯法
- 确定递归函数的返回值和参数
void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result, boolean[] used)
- 确定终止条件,其实不需要
if (startIndex >= nums.length) return;
- 确定单次递归的逻辑
- 与上一题类似
- 增加了去重的过程
- 当
i > 0 && nums[i] == nums[i - 1] && !used[i - 1]
时,直接continue
- 当
result.add(new ArrayList<>(path));
for (int i = startIndex; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
used[i] = true;
path.add(nums[i]);
backtracking(nums, i + 1, path, result, used);
used[i]= false;
path.remove(path.size() - 1);
}
排列问题
46.全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
- 输入: [1,2,3]
- 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
思路:回溯法
- 确定递归函数的返回值和参数
void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result)
- 确定终止条件,中间集合的大小等于数组的大小,就将中间集合添加到结果集合中,并直接返回
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
- 确定单次递归的逻辑
- for循环开始,起始位置都是0,终止条件都是
i < nums.length
- 如果used[i]为真,则直接continue,为了给同一个树枝进行去重
- 其余zjhe
- for循环开始,起始位置都是0,终止条件都是
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.add(nums[i]);
backtracking(nums, used, path, result);
path.remove(path.size() - 1);
used[i] = false;
}
整体代码如下:
package com.yzu.lee.recall;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: Permute
* @Description:给定一个 没有重复 数字的序列,返回其所有可能的全排列。
* 示例:
* 输入: [1,2,3]
* 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
* @author: Leekuangyew
* @date: 2022/6/8 9:43
*/
public class Permute {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) return result;
List<Integer> path = new ArrayList<>();
boolean[] used = new boolean[nums.length];
backtracking(nums, used, path, result);
return result;
}
private void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.add(nums[i]);
backtracking(nums, used, path, result);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
47.全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
- 输入:nums = [1,1,2]
- 输出: [[1,1,2], [1,2,1], [2,1,1]]
思路:回溯法
- 确定递归函数的返回值和参数
void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result)
- 确定终止条件
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
}
- 确定单次递归的逻辑
- 同一树层去重,
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
- 同一树枝去重,
if (used[i]) continue;
- 同一树层去重,
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
if (used[i]) continue;
path.add(nums[i]);
used[i] = true;
backtracking(nums, used, path, result);
path.remove(path.size() - 1);
used[i] = false;
}
整体代码如下:
package com.yzu.lee.recall;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName: PermuteUnique
* @Description:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
* @author: Leekuangyew
* @date: 2022/6/8 11:49
*/
public class PermuteUnique {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) return result;
List<Integer> path = new ArrayList<>();
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
backtracking(nums, used, path, result);
return result;
}
private void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
}
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
if (used[i]) continue;
path.add(nums[i]);
used[i] = true;
backtracking(nums, used, path, result);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
其他问题
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
- 输入: [4, 6, 7, 7]
- 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
- 给定数组的长度不会超过15。
- 数组中的整数范围是 [-100,100]。
- 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
思路:回溯法
- 确定递归函数的返回值和参数
void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result)
- 确定终止条件,当中间集合的大小大于等于2时,将中间集合添加到结果集合中
if (path.size() >= 2) {
result.add(new ArrayList<>(path));
}
- 确定单次递归的逻辑
- 每层新建一个map
- 如果
!path.isEmpty() && path.get(path.size() - 1) > nums[i]
,直接continue - 如果
map.get(nums[i]) != null && map.get(nums[i]) >= 1
,说明同一树层已经有了,直接continue
Map<Integer, Integer> map = new HashMap<>();
for (int i = startIndex; i < nums.length; i++) {
if (!path.isEmpty() && path.get(path.size() - 1) > nums[i]) continue;
//因为数组不是递增的,所以不能使用这种方式直接判断
//if (i>startIndex && nums[i] == nums[i-1]) continue;
if (map.get(nums[i]) != null && map.get(nums[i]) >= 1) continue;
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
path.add(nums[i]);
backtracking(nums, i + 1, path, result);
path.remove(path.size() - 1);
}