回溯法介绍:
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
回溯是递归的副产品,只要有递归就会有回溯。
回溯法解决问题:
回溯法,一般可以解决如下几种问题:
排列与组合的区别:
组合是不强调元素顺序的,排列是强调元素顺序
。例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了
题目:从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。
指数类型枚举:每一个数只有两种选择,选或者不选
代码模板:
boolean[] st;
dfs(1);// 从1开始搜索
// 这里的u枚举的是数,而不是填数的位置
private static void dfs(int u) {
if(u > n){// 递归出口
for(int i = 1; i <= n; i ++){
if(st[i] == true)
System.out.print(i + " ");
}
System.out.println();
return ;
}
// 每个数选或者不选
st[u] = true;
dfs(u + 1);
st[u] = false;
dfs(u + 1);
}
题目:求n的排列(n <= 10)
代码模板:
int[] path;
boolean[] st;
dfs(1);// 从第一个位置开始填数
public static void dfs(int u){
if(u > n){// 递归出口
for(int i = 1; i <= n; i ++){// 处理逻辑
System.out.print(path[i] + " ");
}
System.out.println();
return ;
}
// 枚举每一种可能
for(int i = 1; i <= n; i ++){
if(!st[i]){
st[i] = true;
path[u] = i;
dfs(u + 1);
st[i] = false;
path[u] = 0;
}
}
}
题目:从1~n的数中选出m个数,有多少种可能
不考虑顺序的枚举,是组合型枚举,eg:123和213是一种方案;而在排列型枚举中是属于不同方案
如何实现组合类型枚举:
限制后面位置要放的数字比前一个位置要放的数字大,就可以满足不重复,实现了去重。
相比于排列模板,组合规定了一个
数选择的顺序
!
代码模板:
int[] ways;
dfs(0, 1);// 从第0个位置开始填,从1开始搜
private static void dfs(int u, int start) {
if(u == m){
for(int i = 0; i < m; i ++){
System.out.print(ways[i] + " ");
}
System.out.println();
return ;
}
// 规定顺序,枚举每一种可能
for(int i = start; i <= n; i ++){
ways[u] = i;
dfs(u + 1, i + 1);
ways[u] = 0;// 回溯 恢复现场
}
}
题目链接:77. 组合 - 力扣(LeetCode)
Code
class Solution {
static int n, m;
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n1, int m1) {
n = n1; m = m1;
dfs(0, 1);// 从第0个位置开始填数,从1开始搜索
return res;
}
public void dfs(int u, int start){
if(u == m){
res.add(new ArrayList(path));
return ;
}
for(int i = start; i <= n; i ++){
path.add(i);
dfs(u + 1, i + 1);
path.remove(path.size() - 1);
}
}
}
题目链接:39. 组合总和 - 力扣(LeetCode)
序列是无重复的!
递归枚举组合类型:本题不限定选多少个数,也不限定每一个数选择的次数,为此我们枚举的时候,下一次还是从自己开始,某一个数能不能选关键在于nums[i]
加入path
的条件是sum + nums[i] <= target
否。
如果至少一个数字的被选数量不同,则两种组合是不同的。 ------ 说明
组合是不能够重复
的(2 2 3 和 3 2 2是同一种)
Code
/**
组合模板的变形,同一个数可以取无限次:
枚举时要是当前数比targetget大了,说明该数不能要了直接break;
*/
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int target;
public List<List<Integer>> combinationSum(int[] nums, int val) {
target = val;
Arrays.sort(nums);
dfs(nums, 0, 0);// 从第0位置的数开始枚举
return res;
}
public void dfs(int[] nums, int start, int sum){
// 满足条件的递归出口
if(sum == target){
res.add(new ArrayList(path));
return ;
}
// 按一定的顺序枚举每一种可能
for(int i = start; i < nums.length && nums[i] + sum <= target; i ++){
path.add(nums[i]);
dfs(nums, i, sum + nums[i]);// 下一次还从i开始(无限次嘛)
path.remove(path.size() - 1);
}
}
}
另一种写法:
/**
组合模板的变形,同一个数可以取无限次:
枚举时要是当前数比target大了,说明该数不能要了直接break;
*/
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] nums, int target) {
Arrays.sort(nums);
dfs(nums, 0, target);// 从第0位置的数开始枚举
return res;
}
public void dfs(int[] nums, int start, int target){
// 满足条件的递归出口
if(target == 0){
res.add(new ArrayList(path));
return ;
}
// 按一定的顺序枚举每一种可能
for(int i = start; i < nums.length; i ++){
if(nums[i] > target){// 若nums[i] > targrt说明无法再凑够target
break;
}
path.add(nums[i]);
dfs(nums, i, target - nums[i]);// 下一次还从i开始(无限次嘛)
path.remove(path.size() - 1);
}
}
}
题目链接:40. 组合总和 II - 力扣(LeetCode)
本题和上一题的区别是,上一题每一个数可以取无限个且序列无重复,而本题每一个组合种每一个数只能使用一次且序列可能重复!
由于本题序列可能重复,那么我们枚举组合的时候难免会出现重复集合,那我们如何去重?———— 如何确定枚举顺序?
- 先排序,让重复的元素都聚在一起
- 当我们枚举组合时,每一个数只能枚举一次,遇到重复的就跳过即可。
- 整体思路跟39题大同小异
Code
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int target;
public List<List<Integer>> combinationSum2(int[] nums, int val) {
target = val;
Arrays.sort(nums);
dfs(nums, 0, 0);
return res;
}
public void dfs(int[] nums, int start, int sum){
if(sum == target){
res.add(new ArrayList(path));
return ;
}
for(int i = start; i < nums.length && sum + nums[i] <= target; i ++){
if(i > start && nums[i] == nums[i - 1]){// 保证每一个数只能选一次
continue;
}
path.add(nums[i]);
dfs(nums, i + 1, sum + nums[i]);// 下一次从i + 1开始
path.remove(path.size() - 1);
}
}
}
题目链接:216. 组合总和 III - 力扣(LeetCode)
本题和组合总和II的相同点是每一个只能使用一次,不同点是每一个答案集(组合)大个数(大小)是限定的,限定为k
个数。那么我们就要在枚举的时候做出相应改变,进行适当的剪枝
:
sum == n
时,选择数的个数未达到k
,即不符合sum 未到达 n 之前
,选择数的个数已经超过了k
,即也不符合Code
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int targrt;
int k;
public List<List<Integer>> combinationSum3(int m, int n) {
targrt = n;
k = m;
dfs(1, 0, 0);
return res;
}
public void dfs(int start, int sum, int cnt){
if(cnt == k && sum == targrt){
res.add(new ArrayList(path));
return ;
}
// 剪枝
if(cnt > k || sum > targrt){
return ;
}
// 枚举所有可能组合
for(int i = start; i <= 9 && sum + i <= targrt; i ++){
path.add(i);
dfs(i + 1, sum + i, cnt + 1);
path.remove(path.size() - 1);
}
}
}
这题和组合总和I的联系和区别,都是无限次,但组合可以重复([1,1,2]
、[1,2,1]
、[2,1,1]
)都是一个合理答案————也就是说答案可以重复(更新排列类型)
dfs
写法在组合总和I代码基础上改了改————超时了(数据量比组合总和I还大)
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int target;
int ans = 0;
public int combinationSum4(int[] nums, int val) {
target = val;
Arrays.sort(nums);
dfs(nums, 0, 0);// 从第0位置的数开始枚举
return ans;
}
public void dfs(int[] nums, int start, int sum){
// 满足条件的递归出口
if(sum == target){
ans ++;
res.add(new ArrayList(path));
return ;
}
// 从0开始
for(int i = 0; i < nums.length && nums[i] + sum <= target; i ++){
path.add(nums[i]);
dfs(nums, i, sum + nums[i]);// 既然可以重复,每一次都可以从第0个位置的数枚举
path.remove(path.size() - 1);
}
}
}
本题的正解应该是DP,后续刷到动态规划专题再来补一补吧
题目链接:17. 电话号码的字母组合 - 力扣(LeetCode)
Code
思路一:迭代
class Solution {
static Map<String, String> mp = new HashMap<>(){
{
put("2", "abc");
put("3", "def");
put("4", "ghi");
put("5", "jkl");
put("6", "mno");
put("7", "pqrs");
put("8", "tuv");
put("9", "wxyz");
}
};
public List<String> letterCombinations(String digits) {
List<String> ans = new ArrayList<>();
int n = digits.length();
if(n == 0) return ans;
ans.add("");
for(int i = 0; i < n; i ++){
// 存放到变化后的数组
List<String> t = new ArrayList<>();
String num = digits.substring(i, i + 1);
String s = mp.get(num);
for(int j = 0; j < s.length(); j ++){// 枚举数字对应的所有字母
String e = s.substring(j, j + 1);
// 遍历旧链表存放的组合,并将该字母拼接到所有组合的后面
for(String x : ans){
t.add(x + e);
}
}
ans = t;// 更新组合状态
}
return ans;
}
}
思路二:dfs + 回溯
class Solution {
static Map<String, String> mp = new HashMap<>(){
{
put("2", "abc");
put("3", "def");
put("4", "ghi");
put("5", "jkl");
put("6", "mno");
put("7", "pqrs");
put("8", "tuv");
put("9", "wxyz");
}
};
static List<String> ans = new ArrayList<>();
static void dfs(String digits, int u, String path){
if(u == digits.length()){
ans.add(path);
return ;
}
String t = mp.get(digits.substring(u, u + 1));
for(int i = 0; i < t.length(); i ++){
dfs(digits, u + 1, path + t.charAt(i));
}
}
public List<String> letterCombinations(String digits) {
ans.clear();
int n = digits.length();
if(n == 0) return ans;
dfs(digits, 0, "");
return ans;
}
}