一网打尽回溯子数组!
面试字节和快手都被问了回溯数组的问题,这里整理一下。
1. 组合总和
方法一:回溯+深度优先搜索
分“使用当前数”和“不使用当前数,直接跳过”
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> combine = new ArrayList<Integer>();
dfs(candidates, target, ans, combine, 0);
return ans;
}
public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int idx) {
if (idx == candidates.length) {
return;
}
if (target == 0) {
ans.add(new ArrayList<Integer>(combine));
return;
}
// 不用当前数,直接跳过
dfs(candidates, target, ans, combine, idx + 1);
// 选择当前数
if (target - candidates[idx] >= 0) {
combine.add(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.remove(combine.size() - 1);
}
}
}
方法二:回溯
因为可以无限次使用,所以递归的时候,不用i+1,用i,保证“无限次”的条件
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
search(candidates,target,0,new ArrayList<Integer>(),res,0);
return res;
}
private void search(int[] nums,int target,int sum,List<Integer> tmp,List<List<Integer>> res,int p){
if(sum>target) return;
else if(sum==target){
res.add(new ArrayList<Integer>(tmp));
return;
}
for(int i=p;i<nums.length;i++){
tmp.add(nums[i]);
sum+=nums[i];
search(nums,target,sum,tmp,res,i);
tmp.remove(tmp.size()-1);
sum-=nums[i];
}
}
}
2. 组合总和Ⅱ
本题和上一题的区别在于,本体中数组的每一个元素,只能只用一次,而且元素的值可能重复。也就是说:
1.如果将上题中递归的“i”换成“i+1”,可能会有重复的结果,解决办法是set或者list.indexOf判重,但是会带来空间和时间复杂度;
2.如果将“i”不换成“i+1”,而是先排序,再绕过重复的元素,则可能会少某种结果。比如target=7,list=[1,1,5],因为绕过了第二个1,所以少了结果。
最好的办法就是排序、用freq存储每个元素出现的次数(这时数组freq是去重的,所以直接i+1即可),然后分使用或者不使用此元素,使用的时候,可以使用不同的次数。
class Solution {
List<int[]> freq = new ArrayList<int[]>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> sequence = new ArrayList<Integer>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
for (int num : candidates) {
int size = freq.size();
if (freq.isEmpty() || num != freq.get(size - 1)[0]) {
freq.add(new int[]{num, 1});
} else {
++freq.get(size - 1)[1];
}
}
dfs(0, target);
return ans;
}
public void dfs(int pos, int rest) {
if (rest == 0) {
ans.add(new ArrayList<Integer>(sequence));
return;
}
if (pos == freq.size() || rest < freq.get(pos)[0]) {
return;
}
dfs(pos + 1, rest);
int most = Math.min(rest / freq.get(pos)[0], freq.get(pos)[1]);
for (int i = 1; i <= most; ++i) {
sequence.add(freq.get(pos)[0]);
dfs(pos + 1, rest - i * freq.get(pos)[0]);
}
for (int i = 1; i <= most; ++i) {
sequence.remove(sequence.size() - 1);
}
}
}
3. 组合总和Ⅲ
最正常的回溯了,谢谢leetcode
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> tmp = new ArrayList<Integer>();
dfs(0,k,0,n,1,res,tmp);
return res;
}
private void dfs(int t,int k,int sum,int n,int index, List<List<Integer>> res,List<Integer> tmp){
if(t==k){
if(sum==n){
res.add(new ArrayList<>(tmp));
return;
}
}
if(sum>n) return;
for(int i=index;i<=9;i++){
tmp.add(i);
sum+=i;
dfs(t+1,k,sum,n,i+1,res,tmp);
sum-=i;
tmp.remove(tmp.size()-1);
}
return;
}
}
4. 全排列
每次选择一个数加入到list,加入之后把这个数的index和回溯递归的次数index交换,保证不会选择到重复的数字
class Solution {
public void backtrack(int n, ArrayList<Integer> output,List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n)
res.add(new ArrayList<Integer>(output));
for (int i = first; i < n; i++) {
// 动态维护数组
Collections.swap(output, first, i);
// 继续递归填下一个数
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new LinkedList();
ArrayList<Integer> output = new ArrayList<Integer>();
for (int num : nums)
output.add(num);
int n = nums.length;
backtrack(n, output, res, 0);
return res;
}
}
5. 全排列 II
这个题和之前的组合总和Ⅱ有相似的地方,元素有重复的,还要求结果不重复。除了最后判重之外(这种方式复杂度较高),组合总和Ⅱ是按照使用该元素的个数进行回溯的,但是全排列里面必须全部使用,所以也需要排序,外加 在一次回溯中,如果相邻元素相等,则跳过该元素来实现去重
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
public class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
// 排序(升序或者降序都可以),排序是剪枝的前提
Arrays.sort(nums);
boolean[] used = new boolean[len];
// 使用 Deque 是 Java 官方 Stack 类的建议
Deque<Integer> path = new ArrayDeque<>(len);
dfs(nums, len, 0, used, path, res);
return res;
}
private void dfs(int[] nums, int len, int depth, boolean[] used, Deque<Integer> path, List<List<Integer>> res) {
if (depth == len) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < len; ++i) {
if (used[i]) {
continue;
}
// 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
// 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
// 如果used[i - 1]为true。说明仍在下一次回溯中
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
path.addLast(nums[i]);
used[i] = true;
dfs(nums, len, depth + 1, used, path, res);
// 回溯部分的代码,和 dfs 之前的代码是对称的
used[i] = false;
path.removeLast();
}
}
}
6. 组合
方法一:回溯
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res=new ArrayList<>();
search(n,k,0,1,res,new ArrayList<>());
return res;
}
/*
* tmp:保存当前组合 res:最后的结果集合
* c:用来记录tmp中元素的个数,index:当前的数字大小,保证不会有[2,1]这种情况
*/
public void search(int n,int k,int c,int index,List<List<Integer>> res, List<Integer> tmp){
if(c==k){
res.add(new ArrayList<>(tmp));
return;
}
for(int i=index;i<=n;i++){
tmp.add(i);
search(n,k,c+1,i+1,res,tmp);
tmp.remove(c);
}
}
}
方法二:递归
class Solution {
List<Integer> temp = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> combine(int n, int k) {
dfs(1, n, k);
return ans;
}
public void dfs(int cur, int n, int k) {
// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
if (temp.size() + (n - cur + 1) < k) {
return;
}
// 记录合法的答案
if (temp.size() == k) {
ans.add(new ArrayList<Integer>(temp));
return;
}
// 考虑选择当前位置
temp.add(cur);
dfs(cur + 1, n, k);
temp.remove(temp.size() - 1);
// 考虑不选择当前位置
dfs(cur + 1, n, k);
}
}
7. 子集
在递归的一开始就add
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res=new ArrayList<>();
List<Integer> tmp=new ArrayList<>();
dfs(0,nums,res,tmp);
return res;
}
private void dfs(int k,int[] nums,List<List<Integer>> res, List<Integer> tmp){
if(k>nums.length) return;
res.add(new ArrayList<>(tmp));
for(int i=k;i<nums.length;i++){
tmp.add(nums[i]);
dfs(i+1,nums,res,tmp);
tmp.remove(tmp.size()-1);
}
}
}
8. 子集Ⅱ
在递归的一开始就add,此外,每轮递归的时候跳过重复的元素
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums); //排序
getAns(nums, 0, new ArrayList<>(), ans);
return ans;
}
private void getAns(int[] nums, int start, ArrayList<Integer> temp, List<List<Integer>> ans) {
ans.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]);
getAns(nums, i + 1, temp, ans);
temp.remove(temp.size() - 1);
}
}
}
9. 累加数
class Solution {
public boolean isAdditiveNumber(String num) {
return dfs(num, num.length(), 0, 0, 0, 0);
}
/**
* @param num 原始字符串
* @param len 已用字符串的长度
* @param idx 当前处理下标
* @param sum 前面的两个数字之和
* @param pre 前一个数字
* @param k 当前是处理的第几个数字
*/
private boolean dfs(String num, int len, int idx, long sum, long pre, int k) {
if (idx == len) {
return k > 2;
}
for (int i = idx; i < len; i++) {
long cur = fetchCurValue(num, idx, i);
// 剪枝:无效数字
if (cur < 0) {
continue;
}
// 剪枝:当前数字不等于前面两数之和
if (k >= 2 && cur != sum) {
continue;
}
if (dfs(num, len, i + 1, pre + cur, cur, k + 1)) {
return true;
}
}
return false;
}
/**
* 获取 l ~ r 组成的有效数字
*/
private long fetchCurValue(String num, int l, int r) {
if (l < r && num.charAt(l) == '0') {
return -1;
}
long res = 0;
while (l <= r) {
res = res * 10 + num.charAt(l++) - '0';
}
return res;
}
}