专题一涉及较多类型题目,主要是使用dfs配合for循环实现回溯,常见的题目:组合划分、子集划分、求组和、求子集、求排列、遍历所有情况的搜索、需要列出所有可能结果的搜索
学习自:https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CIII.html
回溯专题一
1、回溯模板
步骤:
- 确定递归函数的参数和返回值
- 回溯函数终止条件
- 单层搜索过程
2、基础题目
2.1组合(中等)
因为需要记录结果,所以必须得用回溯,如果只是统计个数可以用DP,题目中的限制条件很清楚就是1-n和k个数。
class Solution {
// 存储最终答案
List<List<Integer>> ans = new ArrayList<>();
// 存储中间答案
LinkedList<Integer> tmp = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
DFS(1, n, k);
return ans;
}
public void DFS(int start, int n, int k) {
if (tmp.size() == k) {
// 个数达到要求,终止
ans.add(new ArrayList<>(tmp));
return;
}
for (int i = start; i <= n; i++) {
// 处理当前结点
tmp.add(i);
// 注意下一次开始的下标应该是i + 1
DFS(i + 1, n, k);
// 回溯,清除刚刚添加的结点
tmp.removeLast();
}
}
}
一定要注意,for循环中的递归调用DFS(i + 1, n, k),是i + 1,而不是i,是为了避免[2,2]、[3,3]的这种重复情况。
2.2组合数(中等)
和上一题一样的解法,只是题目多了一个大小关系限制,我们从最大的值往下遍历即可。
import java.util.*;
public class Main {
static LinkedList<Integer> ans = new LinkedList<>();
public static void main(String[] args) {
int n, r;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
r = scan.nextInt();
DFS(n, r, n);
}
static void DFS(int n, int r, int start) {
// 满足题目要求,打印
if (ans.size() == r) {
for (int i = 0; i < ans.size(); i++) {
System.out.print(ans.get(i));
}
System.out.println();
return;
}
// 按题目要求从大到小遍历
for (int i = start; i >= 1; i--) {
ans.add(i);
DFS(n, r, i - 1);
ans.removeLast();
}
}
}
2.3自然数拆分(中等)
一样的道理,但是本题可以重复使用数字,所以在递归调用的时候要注意参数!
import java.util.*;
public class Main {
static LinkedList<Integer> ans = new LinkedList<>();
public static void main(String[] args) {
int n;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
DFS(n, 1, 0);
}
static void DFS(int n, int start, int sum) {
if (sum > n) {
return;
}
if (sum == n) {
// 满足答案要求,打印
System.out.printf("%d=", n);
for (int i = 0; i < ans.size(); i++) {
if (i == 0) {
System.out.print(ans.get(i));
} else {
System.out.printf("+%d", ans.get(i));
}
}
System.out.println();
return;
}
for (int i = start; i < n; i++) {
ans.add(i);
// 本题目允许重复使用数字
DFS(n, i, sum + i);
ans.removeLast();
}
}
}
2.4数的划分(困难)
和上面题目一样的遍历方法,允许重复数字,下面是未剪枝的情况,会有两个例子超时。
import java.util.*;
public class Main {
static int ans = 0;
// static LinkedList<Integer> tmp = new LinkedList<>();
public static void main(String[] args) {
int n, k;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
k = scan.nextInt();
// 每份至少分 1 ,所以下标得从1开始
DFS(n, k, 1, 0, 0);
System.out.println(ans);
}
static void DFS(int n, int k, int start, int cnt, int sum) {
if (sum > n || cnt > k) {
// 超出题目要求,提前返回
return;
}
if (sum == n && cnt == k) {
// 满足题目要求,累加答案
ans++;
// System.out.println(tmp);
return;
}
for (int i = start; i <= n; i++) {
// tmp.add(i); // 方便调试
// 题目允许重复使用数字,所以下次下标还可以从i 开始
DFS(n, k, i, cnt + 1, sum + i);
// tmp.removeLast();
}
}
}
剪枝,发现cnt是多余的,可以直接用m来计数,并且下面的for循环不是非得到n,只用到n - sum即可,画出搜索树就可以知道该怎么剪枝了。
import java.util.*;
public class Main {
static int ans = 0;
// static LinkedList<Integer> tmp = new LinkedList<>();
public static void main(String[] args) {
int n, k;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
k = scan.nextInt();
// 每份至少分 1 ,所以下标得从1开始
DFS(n, k, 1, 0);
System.out.println(ans);
}
static void DFS(int n, int k, int start, int sum) {
if ((k == 0 && sum < n) || sum > n) {
// 超出题目要求,提前返回
return;
}
if (k == 0 && sum == n) {
// 满足题目要求,累加答案
ans++;
// System.out.println(tmp);
return;
}
for (int i = start; i <= n - sum; i++) {
// tmp.add(i); // 方便调试
// 题目允许重复使用数字,所以下次下标还可以从i 开始
DFS(n, k - 1, i, sum + i);
// tmp.removeLast();
}
}
}
这道题其实也可以用DP解,但是很难想出状态转移方程,使用回溯很直接,加上剪枝也可以达到一样的效果,并且遇到需要打印出结果的题目,也可以做出解答。
2.5放苹果(困难)
本题是上题的改编,允许盘子为空。以题目为例,可能的情况为:007 016 025 034 115 124 133 223,可以画出搜索树:
从上图可以看到,0搜过之后开始结点还可以是0。一定要注意,做好剪枝,避免超时。
import java.util.*;
public class Main {
static int ans = 0;
// static LinkedList<Integer> tmp = new LinkedList<>();
public static void main(String[] args) {
int t;
Scanner scan = new Scanner(System.in);
t = scan.nextInt();
int m, n;
while(t > 0) {
m = scan.nextInt();
n = scan.nextInt();
ans = 0;
DFS(m, n, 0, 0);
System.out.println(ans);
t--;
}
}
static void DFS(int m, int n, int start, int sum) {
if (n == 0 && sum < m || sum > m) {
return;
}
// if (n == 1 && sum == 0) {
// System.out.println(tmp);
// ans++;
// return;
// }
if (n == 0 && sum == m) {
// System.out.println(tmp);
ans++;
return;
}
for (int i = start; i <= m - sum; i++) {
// tmp.add(i);
DFS(m, n - 1, i, sum + i);
// tmp.removeLast();
}
}
}
2.6合成分子(中等)
仔细分析题目,就相当于是把n拆分成10份,每份必须为1或2或3。并且题目要求必须打印出可能方案,就必须得搜索。
import java.util.*;
public class Main {
static int ans = 0;
static List<LinkedList<Integer>> ans_ = new ArrayList<>();
static LinkedList<Integer> tmp = new LinkedList<>();
public static void main(String[] args) {
int n;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
// n 分成 10 份,每份必须为1、2、3份
DFS(n, 0, 0);
System.out.println(ans);
for (int i = 0; i < ans_.size(); i++) {
for (int j = 0; j < ans_.get(i).size(); j++) {
if (j == 0) {
System.out.printf("%d", ans_.get(i).get(j));
} else {
System.out.printf(" %d", ans_.get(i).get(j));
}
}
System.out.println();
}
}
static void DFS(int n, int cnt, int sum) {
if (cnt > 10 || sum > n) {
return;
}
if (cnt == 10 && sum == n) {
ans++;
ans_.add(new LinkedList<>(tmp));
return;
}
//注意这里要从1-3遍历,才能满足题意
for (int i = 1; i <= 3; i++) {
tmp.add(i);
DFS(n,cnt + 1, sum + i);
tmp.removeLast();
}
}
}
2.7文具店(困难)
告诉了购买的水彩笔支数,那么就可以基于支数进行遍历,把字符串s拆分成三坨进行求和,找里面最小的和即可。
import java.util.*;
public class Main {
static int ans = Integer.MAX_VALUE;
// static LinkedList<Integer> test = new LinkedList<>();
public static void main(String[] args) {
String s = "";
int k = 0;
Scanner scan = new Scanner(System.in);
s = scan.next();
k = scan.nextInt();
dfs(s, k, 0, 0, 0);
System.out.println(ans);
}
static void dfs(String s, int k, int sum, int start, int len) {
// 剪枝
if (k == 0 && len < s.length() || len > s.length()) {
return;
}
// 满足题目要求可以进行求最小值
if (k == 0 && len == s.length()) {
ans = Math.min(sum, ans);
// System.out.println(test);
return;
}
for (int i = start; i < s.length(); i++) {
// 每次可能取7,72,725,7255,72553
// 如果取了7,进入更深的dfs要从2开始取,2,25,255,2553
// 如果不取7,那就继续往下取,取72,725,7255...直到遍历完全
// 一定要保证最后遍历完了所有的字符
String str = s.substring(start, i + 1);
int tmp_len = str.length();
int tmp = Integer.parseInt(str);
// test.add(tmp);
dfs(s, k - 1, sum + tmp, i + 1, len + tmp_len);
// test.removeLast();
}
}
}
2.8奇怪的电梯(中等)
用dfs、bfs都可以,但需要对访问过的电梯楼层进行标记,因为在楼层跳转过程中很有可能又回到了之前已经遍历过的楼层,所以需要对访问过的楼层标记,不标记的话会陷入死循环。
对于每一层有两种操作,可以向上也可以向下,可以判断当前楼层能否向上、向下,再判断将要前往的楼层之前是否访问过了,本题与之前题目较大不同在于:需要标记访问过的楼层,因为这些访问过的楼层后续可能还访问到,重复了,而之前的题目都避免了重复访问的问题。
import java.util.*;
public class Main {
static int ans = Integer.MAX_VALUE;
static int[] dt = new int[220];
static boolean[] vis = new boolean[220];
public static void main(String[] args) {
int n, a, b;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
a = scan.nextInt();
b = scan.nextInt();
for (int i = 1; i <= n; i++) {
dt[i] = scan.nextInt();
}
// n层楼,从 a层到 b层要按几次按钮
dfs(n, a, b, 0);
System.out.println(ans == Integer.MAX_VALUE ? -1 : ans);
}
static void dfs(int n, int a, int b, int cnt) {
if (a <= 0 || a > n) {
return;
}
if (cnt >= ans) {
return;
}
if (a == b) {
ans = Math.min(ans, cnt);
return;
}
vis[a] = true;
// 可能向上
if (a + dt[a] <= n && vis[a + dt[a]] == false) {
dfs(n, a + dt[a], b, cnt + 1);
}
// 可能向下
if (a - dt[a] >= 1 && vis[a - dt[a]] == false) {
dfs(n, a - dt[a], b, cnt + 1);
}
// 回溯
vis[a] = false;
}
}
2.9分成互质组(困难)
互质:最大公约数 == 1,本题的难点在于要记录数属于哪个组,还需要把每个数与之前的组的每个数比较,如果互质的,那当前的数也放在那个组里;如果遍历完所有的已有组都不是互质的,那就需要新开一个组。整个逻辑比较绕,需要慢慢写。
import java.util.*;
public class Main {
static int ans = Integer.MAX_VALUE;
static int[] dt = new int[30];
static boolean flag = false;
// 记录每个元素组别
static int[] group_s = new int[30];
public static void main(String[] args) {
int n;
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
for (int i = 1; i <= n; i++) {
dt[i] = scan.nextInt();
}
// 互质:几个数的最大公约数 = 1
// 从第一个数开始遍历,目前有1个组
dfs(n, 1, 1);
System.out.println(ans);
}
static void dfs(int n, int num, int group) {
// num:现在遍历到第几个数,group:现在有几个组
// num == n + 1,说明已经遍历完最后一个数
if (num == n + 1) {
ans = Math.min(ans, group);
}
if (num > n + 1) {
return;
}
// 从第一组开始遍历,依次判断当前第num个数是否每个组都互质
for (int i = 1; i <= group; i++) {
flag = true;
// 注意第一个数是进不了第二个循环的,因为第一个数可以直接放到第一个组里(第一个组暂时什么都没有)
for (int j = 1; j < num; j++) {
// gcd != 1 不是互质的,那说明不是同一组的
if (group_s[j] == i && check(dt[j], dt[num]) != 1) {
flag = false;
break;
}
}
// true说明当前数是已有的组里的
if (flag == true) {
// 保存组数
group_s[num] = i;
// 加数下标,不加组数
dfs(n, num + 1, group);
}
}
// false说明当前数不属于任何一个组,要新开一个组
if (flag == false) {
group_s[num] = group + 1;
// 加数下标,加组数
dfs(n, num + 1, group + 1);
}
}
// 互质:最大公约数 = 1
static int check(int a, int b) {
if (b == 0) {
return a;
}
return check(b, a % b);
}
}
2.10组合总和(中等)
注意每个数可以重复用,求组合数(不强调顺序)
class Solution {
LinkedList<Integer> tmp = new LinkedList<>();
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// 不同组合,元素可以多次使用
dfs(candidates, target, 0, 0);
return ans;
}
public void dfs(int[] candidates, int target, int sum, int start) {
if (sum > target) {
return;
}
if (sum == target) {
ans.add(new ArrayList<>(tmp));
return;
}
for (int i = start; i < candidates.length; i++) {
tmp.add(candidates[i]);
// 注意下一次dfs的开始下标,从i开始
dfs(candidates, target, sum + candidates[i], i);
tmp.removeLast();
}
}
}
注意上面的dfs语句,只有让下一次开始的下标为上一次的 i 才能避免顺序不同,但实际是同一组和的情况。如果不记录开始下标,而是每次从第一个元素开始,则是求排列,两者比较如下图所示:
2.11组合总和Ⅱ(中等)
相比于上一道题,本题要求数组中的每个元素只能用一次,但是数组中的元素很有可能重复,例如 1 7 1组合成8,1 7 和 7 1都可以,这就重复了,怎么解决呢?
我们可以先对数组排序,排序了之后如果有重复的数,例如 1 7 1会排序为 1 1 7,这样我们只用考虑第一个1,遇到之后相等的情况就不要考虑了。
class Solution {
LinkedList<Integer> tmp = new LinkedList<>();
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 不同组合,元素只能用一次,但是给出的元素又有重复的
Arrays.sort(candidates);
dfs(candidates, target, 0, 0);
return ans;
}
public void dfs(int[] candidates, int target, int sum, int start) {
if (sum > target) {
return;
}
if (sum == target) {
ans.add(new ArrayList<>(tmp));
return;
}
for (int i = start; i < candidates.length; i++) {
// 当数组中元素重复时,只考虑第一个重复元素
if(i > start && candidates[i] == candidates[i-1])
continue;
tmp.add(candidates[i]);
dfs(candidates, target, sum + candidates[i], i + 1);
tmp.removeLast();
}
}
}
2.12组合总和Ⅲ(中等)
比之前的组合总和简单多了,数组中也没有重复的元素,直接遍历1-9,下一个dfs下标从i + 1开始即可。
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
// 1 - 9的正整数
dfs(k, n, 0, 1);
return ans;
}
public LinkedList<Integer> tmp = new LinkedList<>();
public List<List<Integer>> ans = new ArrayList<>();
public void dfs(int k, int n, int sum, int start) {
if (sum > n || k < 0) {
return;
}
if (sum == n && k == 0) {
ans.add(new ArrayList<>(tmp));
return;
}
for (int i = start; i <= 9; i++) {
tmp.add(i);
// 注意不允许有重复的数字
dfs(k - 1, n, sum + i, i + 1);
// 回溯
tmp.removeLast();
}
}
}
2.13组合总和Ⅳ(中等)
这道题可以用dfs解,但是会超时,由于只需要返回种类数,考虑用dp解,就是之前在dp专题讲的爬楼梯问题,只不过这次每次可以爬的台阶数是nums中的各个数。
// 回溯:超时
class Solution {
int ans = 0;
public int combinationSum4(int[] nums, int target) {
// 不同的数,组成target,求组合数,每个数可以用多次(112 121属于不同组合)
dfs(nums, target, 0);
return ans;
}
void dfs(int[] nums, int target, int sum) {
if (sum > target) {
return;
}
if (sum == target) {
ans++;
return;
}
for (int i = 0; i < nums.length; i++) {
dfs(nums, target, sum + nums[i]);
}
}
}
// 动态规划
class Solution {
public int combinationSum4(int[] nums, int target) {
int n = nums.length;
// 类似于爬楼梯,但是每次可以走nums[i]的步数,需要target阶梯爬到楼顶
// 问能爬到楼顶的方案数
int[] dp = new int[target + 1];
for (int i = 0; i < n; i++) {
if (nums[i] <= target) {
dp[nums[i]] = 1;
}
}
// 第 i 个阶梯,可以由 i - nums[j] 的台阶 + nums[j]爬上来!
for (int i = 1; i <= target; i++) {
for (int j = 0; j < n; j++) {
if (nums[j] <= i) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
}
2.14电话号码的字母组合(中等)
难点在于既要遍历按键,又要遍历按键内部的字符。不管它遍历几个东西,我们都得确定它们的排列顺序,显然是先确定某个按键,再确定该按键内某个字符。就跟上面互质组一样,对于某个数,要确定它和每一个组是不是互质,那不就是说先确定组,再确定组里的元素,这样就可以遍历了。
还需要注意本题中某个按键是否能重复按下,显然是不能的,所以在开始下一轮dfs迭代时也需要注意。
class Solution {
HashMap<Integer, String> number = new HashMap<>();
List<String> ans = new ArrayList<>();
StringBuilder str = new StringBuilder();
public List<String> letterCombinations(String digits) {
if (digits.equals("")) {
return new ArrayList<>();
}
number.put(2, "abc");
number.put(3, "def");
number.put(4, "ghi");
number.put(5, "jkl");
number.put(6, "mno");
number.put(7, "pqrs");
number.put(8, "tuv");
number.put(9, "wxyz");
// cnt代表遍历到第几个字符,inter代表内部遍历序号
dfs(digits, 0, 0);
return ans;
}
void dfs(String digits, int cnt, int start) {
if (str.length() > digits.length()) {
return;
}
if (str.length() == digits.length()) {
ans.add(str.toString());
System.out.println(str);
return;
}
// 外层遍历按键数字,内层遍历每个数字内部的字母
for (int i = start; i < digits.length(); i++) {
String tmp = number.get(digits.charAt(i) - '0');
for (int j = 0; j < tmp.length(); j++) {
str.append(tmp.charAt(j));
dfs(digits, cnt + 1, i + 1);
// 回溯
str.delete(str.length() - 1, str.length());
}
}
}
}
2.15分割回文串(中等)
有了上面的题目经验,应该有了经验,回溯无非就是穷尽所有可能,但是在穷尽过程中需要根据题目要求去掉部分的可能,为了提升速度加上限制条件这就是剪枝。回到本题,就是尝试以1 2 3…的长度去划分字符串,再判断这些字符串是否为回文,是的话再次基础上再接着划分,注意要避免前面已经划分过的串。
class Solution {
List<List<String>> ans = new ArrayList<>();
LinkedList<String> tmp = new LinkedList<>();
public List<List<String>> partition(String s) {
// 把字符串s分割成多个子串,每个子串都是回文,输出可能的结果
dfs(s, 0, 0);
return ans;
}
void dfs(String s, int start, int cnt) {
if (cnt > s.length()) {
return;
}
if (cnt == s.length()) {
ans.add(new ArrayList<>(tmp));
return;
}
for (int i = start; i < s.length() ; i++) {
String str = s.substring(start, i + 1);
// 截取一个子串出来看是不是回文
if (check(str)) {
tmp.add(str);
// 注意下一次开始截取的字符串下标是多少,避免重复截取
dfs(s, i + 1, cnt + str.length());
tmp.removeLast();
}
}
}
// 判断当前子串是不是回文
boolean check(String tmp) {
int len = tmp.length();
if (len <= 1) {
return true;
}
for (int i = 0; i < len / 2; i++) {
if (tmp.charAt(i) != tmp.charAt(len - i - 1)) {
return false;
}
}
return true;
}
}
2.16复原IP地址(中等)
就是普通的搜索题目,注意下限制条件即可,可以通过统计点或者数的个数,来剪枝。
class Solution {
List<String> ans = new ArrayList<>();
String tmp = "";
public List<String> restoreIpAddresses(String s) {
// 无非就是4个数,3个点
dfs(s, 0, 0, 0);
return ans;
}
void dfs(String s, int num, int point, int start) {
if (num == 4 && point == 3 && tmp.length() == s.length() + 3) {
ans.add(tmp);
}
if (num > 4 || point > 3) {
return;
}
// num :数字个数,point:点的个数
for (int i = start; i < s.length(); i++) {
String str = s.substring(start, i + 1);
if (str.length() > 4) {
continue;
}
int n = Integer.parseInt(str);
if (str.length() != Integer.toString(n).length()) {
// 有前置零
continue;
}
if (n >= 0 && n <= 255) {
String tmpp = tmp;
if (num == 0) {
tmp = tmp + str;
dfs(s, num + 1, point, i + 1);
} else {
tmp = tmp + "." + str;
dfs(s, num + 1, point + 1, i + 1);
}
// 回溯
tmp = tmpp;
}
}
}
}
2.17子集Ⅱ(中等)
数组中的元素可以重复了,如何处理重复元素?好像上面的题目有类似的吧,就是组合总和Ⅱ的方法,先排序,这样重复的元素都在一起了,对于重复的元素我们只用考虑它们的第一次的使用情况,例如:117,只考虑第一个1的使用情况:1 11 117,考虑了第一个就不考虑第二个了(如果再考虑是没有价值的,只会出现和前面的1重复的情况)
class Solution {
List<List<Integer>> ans = new ArrayList<>();
LinkedList<Integer> tmp = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
// 排序处理元素重复的情况
Arrays.sort(nums);
dfs(nums, 0);
return ans;
}
void dfs(int[] nums, int start) {
ans.add(new ArrayList<>(tmp));
for (int i = start; i < nums.length; i++) {
// 排除元素重复的使用
if (i > start && nums[i - 1] == nums[i]) {
continue;
}
tmp.add(nums[i]);
dfs(nums, i + 1);
tmp.removeLast();
}
}
}
有了上面组合总和Ⅱ和子集Ⅱ的例子,一定要学会处理数组中有重复元素,但是又得避免重复组合、子集的答案的处理方法:先排序,这样重复的元素就在一起了,只用考虑重复的元素一次即可。
2.18※递增子序列(中等)
下面的代码为什么不行了?
package Chapter_5;
import java.util.*;
public class Main {
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findSubsequences(new int[] {1,2,3,4,5,6,7,8,9,10,1,1,1,1,1}));
}
}
class Solution {
List<List<Integer>> ans = new ArrayList<>();
LinkedList<Integer> tmp = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
// 递增子序列,元素不能重复使用,子序列中至少有两个元素
dfs(nums,0);
return ans;
}
void dfs(int[] nums, int start) {
if (tmp.size() > nums.length) {
return;
}
if (tmp.size() >= 2) {
ans.add(new ArrayList<>(tmp));
}
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i] == nums[i - 1]) {
return;
}
if (tmp.size() == 0) {
tmp.add(nums[i]);
dfs (nums, i + 1);
tmp.removeLast();
} else {
if (nums[i] >= tmp.get(tmp.size() - 1)) {
tmp.add(nums[i]);
dfs (nums, i + 1);
tmp.removeLast();
}
}
}
}
}
很明显可以看到这样一个例子:1,2,3,4,5,6,7,8,9,10,1,1,1,1,1,前面的1考虑后,并没有过滤掉后面重复的1。排序再去重呢?按照题目意思,如果排序就改变题目意思了,题目是说在原始的nums数组中进行寻找。所以为了避免后面的又被重复考虑了,只能把用过的数存入set,避免后续的重复考虑。(注意set的声明位置!!!当然也可以对结果去重)
class Solution {
List<List<Integer>> ans = new ArrayList<>();
LinkedList<Integer> tmp = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
// 递增子序列,元素不能重复使用,子序列中至少有两个元素
dfs(nums,0);
return ans;
}
void dfs(int[] nums, int start) {
if (tmp.size() > nums.length) {
return;
}
if (tmp.size() >= 2) {
ans.add(new ArrayList<>(tmp));
}
// 注意set的声明位置,只用排除每一层的重复元素
// 因为一层只应该考虑相同的元素一次
Set<Integer> used = new HashSet<>();
for (int i = start; i < nums.length; i++) {
if (used.contains(nums[i])) {
continue;
}
if (tmp.size() == 0) {
used.add(nums[i]);
tmp.add(nums[i]);
dfs (nums, i + 1);
tmp.removeLast();
} else {
if (nums[i] >= tmp.get(tmp.size() - 1)) {
used.add(nums[i]);
tmp.add(nums[i]);
dfs (nums, i + 1);
tmp.removeLast();
}
}
}
}
}
2.19全排列(中等)
之前一直都是组合问题,没有涉及排列问题,以输入:1 2 3为例,如何输出2 1 3,在之前组合类题目中,确定了2之后不会再遍历到2之前的元素了,所以无论在哪一层都应该从第一个元素开始遍历。但是重复问题如何解决?用一个数组记录当前遍历过程中用过的元素即可,在回溯的过程中把用过的元素再清0即可(保证后面可以再次遍历到它)。
class Solution {
LinkedList<Integer> tmp = new LinkedList<>();
List<List<Integer>> ans = new ArrayList<>();
int[] vis = new int[30];
public List<List<Integer>> permute(int[] nums) {
dfs(nums);
return ans;
}
void dfs(int[] nums) {
if (tmp.size() == nums.length) {
ans.add(new ArrayList<>(tmp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (vis[i] == 1) {
continue;
}
// 访问过的元素标记
vis[i] = 1;
tmp.add(nums[i]);
dfs(nums);
// 回溯
vis[i] = 0;
tmp.removeLast();
}
}
}
2.20※全排列Ⅱ(中等)
上面记录每个元素是否访问肯定是无法使用了,因为有1 1 2这种情况出现。可以先排序,和之前找子集一样的去重方法,对于1 1 2,要避免第二个“1”的重复结果,如果第一个1已被访问,说明当前的情况是1重新被遍历,因为为了构成排列,每次遍历都从第一个元素开始,就导致了结果的重复。(可以画图看看如何避免重复的情况)
class Solution {
LinkedList<Integer> tmp = new LinkedList<>();
List<List<Integer>> ans = new ArrayList<>();
int[] vis = new int[30];
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
dfs(nums);
return ans;
}
void dfs(int[] nums) {
if (tmp.size() == nums.length) {
ans.add(new ArrayList<>(tmp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (vis[i] == 1) {
continue;
}
// 避免重复元素的重复结果
if (i > 0 && nums[i] == nums[i - 1] && vis[i - 1] == 0) {
continue;
}
vis[i] = 1;
tmp.add(nums[i]);
dfs(nums);
// 回溯
vis[i] = 0;
tmp.removeLast();
}
}
}
上面几道题其实模板都是一样的,区别是:每个元素能否重复使用、是否需要按照数组顺序、是否需要考虑顺序、考虑的是组合还是排列。较难的地方就是去重,需要想清楚应该哪里去重能够优化时间(相当于是剪枝,因为这些问题其实都可以对结果去重,只是时间花销大),一般可以通过画搜索树观察结构进行剪枝,一定要分清楚每层和分枝的关系。
for (int i = start; i < n; i++) {
tmp.add(i);
dfs(nums, i + 1);
tmp.removeLast();
}
上面的for循环如果不管dfs语句,那就是在遍历一层,如果一直只考虑dfs语句,一直深入,那就是在遍历当前分枝到尽头。
2.21字母大小写全排列(中等)
这道题简单,只用管字母,把小写大写互相转换,遍历完所有情况就可以。关键是要会使用String和char[],String可以直接转成char[],char[]也可以通过new String()生成String,用char[]的好处是可以直接通过下标修改字符,修改完了回溯也方便。
class Solution {
List<String> ans = new ArrayList<>();
public List<String> letterCasePermutation(String s) {
// String也是可以直接转成char[]的
char[] chs = s.toCharArray();
dfs(chs, 0);
return ans;
}
void dfs(char[] chs, int start) {
// char[]数组是可以生产String的
ans.add(new String(chs));
for (int i = start; i < chs.length; i++) {
char tmp = chs[i];
if (tmp >= '0' && tmp <= '9') {
continue;
} else if (tmp >= 'a' && tmp <= 'z') {
chs[i] = (char) (chs[i] - 32);
} else if (chs[i] >= 'A' && chs[i] <= 'Z') {
chs[i] = (char) (chs[i] + 32);
}
dfs(chs, i + 1);
chs[i] = tmp;
}
}
}