声明:把递归参数不变的,尽量统一设置为成员变量。
一、全排列
全排列问题求解体系基本上分为两大类,一是基于选择,二是基于交换。
1.1 全排列1:无重复
1.1.1 基于选择的经典解法
优点是好记,递归时每次循环都从 0 开始。
class Solution {
private int[] nums;
private List<List<Integer>> result;
public List<List<Integer>> permute(int[] nums) {
this.nums = nums;
result = new ArrayList<>();
boolean[] used = new boolean[nums.length];
backtrack(new ArrayList<Integer>(), used);
return result;
}
private void backtrack(List<Integer> temp, boolean[] used){
if(temp.size() == nums.length){
result.add(new ArrayList<>(temp));
return;
}
for(int i = 0; i < nums.length; ++i){
if(used[i]) continue;
used[i] = true;
temp.add(nums[i]);
backtrack(temp, used);
temp.remove(nums[i]);
used[i] = false;
}
}
}
1.1.2 基于交换的经典解法
class Solution {
private List<List<Integer>> result;
public List<List<Integer>> permute(int[] nums) {
result = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
for(int e : nums) temp.add(e);
backtrack(temp, 0);
return result;
}
private void backtrack(List<Integer> temp, int start){
if(start == temp.size()) result.add(new ArrayList<>(temp));
for(int i = start; i < temp.size(); ++i){
Collections.swap(temp, i, start);
backtrack(temp, start+1);
Collections.swap(temp, i, start);
}
}
}
1.1.3 另一种基于交换的解法
class Solution {
private List<List<Integer>> result;
public List<List<Integer>> permute(int[] nums) {
result = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
for(int e : nums) temp.add(e);
backtrack(temp, 0);
return result;
}
private void backtrack(List<Integer> temp, int start){
result.add(new ArrayList<>(temp));
for(int i = start; i < temp.size(); ++i) {
for(int j = i+1; j < temp.size(); ++j) {
Collections.swap(temp, i, j);
backtrack(temp, i+1);
Collections.swap(temp, i, j);
}
}
}
}
1.2 全排列2:有重复
1.2.1 基于选择的解法
这种解法,现在又不好记了,因为去重的判断比较复杂。
class Solution {
private int[] nums;
private List<List<Integer>> result;
public List<List<Integer>> permuteUnique(int[] nums) {
this.nums = nums;
result = new ArrayList<>();
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
backtrack(new ArrayList<>(), used);
return result;
}
private void backtrack(List<Integer> temp, boolean[] used){
if(temp.size() == nums.length){
result.add(new ArrayList<>(temp));
return;
}
for(int i = 0; i < nums.length; ++i){
if(used[i]) continue;
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
used[i] = true;
temp.add(nums[i]);
backtrack(temp, used);
used[i] = false;
temp.remove(temp.size()-1);
}
}
}
1.2.2 基于交换的解法(不能通过排序去重,因为交换会打乱有序性)
有重复全排列的推荐解法!!!
class Solution {
private List<List<Integer>> result;
public List<List<Integer>> permuteUnique(int[] nums) {
result = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
for(int num : nums) temp.add(num);
backTrack(temp, 0);
return result;
}
public void backTrack(List<Integer> temp, int start) {
if(start == temp.size()) result.add(new ArrayList<>(temp));
Set<Integer> set = new HashSet<>(); // 数据较少的情况下,for 循环判重更好
for(int i = start; i < temp.size(); ++i){
if(set.contains(temp.get(i))) continue;
Collections.swap(temp, i, start);
backTrack(temp, start+1);
Collections.swap(temp, i, start);
set.add(temp.get(i));
}
}
}
枚举当前层级的候选集时,第一个 2 已经和 1 交换,如果还让第二个 2 和 1 交换,那么下步交换就会导致重复。
用数学语言严谨地描述一下:
约定:[start, …] 表示 start 及其以后的所有元素。
枚举当前层级的候选集时,分别用 [start, …] 和 start 交换,如果 [start, …] 中有两个相同的元素分别和 start 交换,会得到两个候选集,这两个候选集的 [0, start] 部分是完全相等的,[start+1, …] 部分除了顺序外也是相等的。这两个候选集在后续层级的递归中,会分别得到 [start+1, …] 的全排列,因为 start 只会变大,[0, start] 部分不会改变。所以这两个候选集就会得到两个相同的结果子集,即重复。
1.2.3 基于交换的另一种解法:
class Solution {
private List<List<Integer>> result;
public List<List<Integer>> permuteUnique(int[] nums) {
result = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
for(int e : nums) temp.add(e);
backtrack(temp, 0);
return result;
}
private void backtrack(List<Integer> temp, int li) {
result.add(new ArrayList<>(temp));
for(int i = li; i < temp.size(); ++i) {
Set<Integer> set = new HashSet<>();
set.add(temp.get(i));
for(int j = i+1; j < temp.size(); ++j) {
if(set.contains(temp.get(j))) continue;
Collections.swap(temp, i, j);
backtrack(temp, i+1);
Collections.swap(temp, i, j);
set.add(temp.get(j));
}
}
}
}
二、组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
无重复元素,基于选择:
class Solution {
private List<List<Integer>> result;
public List<List<Integer>> combine(int n, int k) {
result = new ArrayList<>();
backtrack(n, k, 1, new ArrayList<>(k));
return result;
}
private void backtrack(int n, int k, int start, List<Integer> temp){
if(k == 0){
result.add(new ArrayList<>(temp));
return;
}
for(int i = start; i <= n-k+1; ++i){
temp.add(i);
backtrack(n, k-1, i+1, temp);
temp.remove(temp.size()-1);
}
}
}
三、组合总和
和为给定值的所有组合。
3.1 组合总和1:无重复元素,元素可以多次被选中
class Solution {
private List<List<Integer>> result;
private int[] candidates;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
result = new ArrayList<>();
this.candidates = candidates;
backtrack(new ArrayList<Integer>(), 0, target);
return result;
}
private void backtrack(List<Integer> temp, int start, int target){
if(target == 0){
result.add(new ArrayList<>(temp));
return;
}
for(int i = start; i < candidates.length; ++i){
if(candidates[i] <= target){
temp.add(candidates[i]);
backtrack(temp, i, target-candidates[i]);
temp.remove(temp.size()-1);
}
}
}
}
3.2 组合总和2:有重复元素,每个元素只能使用一次
class Solution {
private List<List<Integer>> result;
private int[] candidates;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
result = new ArrayList<>();
this.candidates = candidates;
Arrays.sort(candidates);
backtrack(target, new ArrayList<>(), 0);
return result;
}
private void backtrack(int target, List<Integer> temp, int start){
if(target == 0){
result.add(new ArrayList<>(temp));
return;
}
for(int i = start; i < candidates.length; ++i){
if(i > start && candidates[i] == candidates[i-1]) continue;
if(candidates[i] <= target){
temp.add(candidates[i]);
backtrack(target-candidates[i], temp, i+1);
temp.remove(temp.size()-1);
}else return;
}
}
}
四、子集
返回给定数组所有可能的子集(幂集)。
4.1 子集1:无重复元素
二进制位掩码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
int n = 1 << nums.length;
for(int i = 0; i < n; ++i) {
List<Integer> subSet = new ArrayList<>();
for(int j = 0; j < nums.length; ++j) {
if((i & (1 << j)) != 0) {
subSet.add(nums[j]);
}
}
result.add(subSet);
}
return result;
}
}
4.2 子集2:有重复元素
class Solution {
private List<List<Integer>> result;
private int[] nums;
public List<List<Integer>> subsetsWithDup(int[] nums) {
result = new ArrayList<>();
this.nums = nums;
Arrays.sort(nums);
backtrack(0, new ArrayList<>());
return result;
}
private void backtrack(int k, List<Integer> temp){
result.add(new ArrayList<>(temp));
for(int i = k; i < nums.length; ++i){
if(i > k && nums[i] == nums[i-1]) continue;
temp.add(nums[i]);
backtrack(i+1, temp);
temp.remove(temp.size()-1);
}
}
}
除了全排列有基于交换的方法,其余的基本都使用基于选择的方法。