回溯算法
一、理论基础
参考代码随想录,仅作记录学习之用
- 回溯是递归的副产品,只要有递归就会有回溯
- 因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法⾼效⼀些,可以加⼀些剪枝的操作(也可能会有逻辑操作),但也改不了回溯法就是穷举的本质
- 回溯法解决的问题都可以抽象为树形结构
- 回溯法解决的都是在集合中递归查找⼦集,集合的⼤⼩就构成了树的宽度,递归的深度,都构成的树的深度
- 递归就要有终⽌条件,所以必然是⼀棵⾼度有限的树(N叉树)
回溯法,⼀般可以解决如下⼏种问题:
组合问题
:N个数⾥⾯按⼀定规则找出k个数的集合
切割问题
:⼀个字符串按⼀定规则有⼏种切割⽅式
⼦集问题
:⼀个N个数的集合⾥有多少符合条件的⼦集
排列问题
:N个数按⼀定规则全排列,有⼏种排列⽅式
棋盘问题
:N皇后,解数独等等
二、回溯算法模板
void backtracking(参数) {
if (终⽌条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77、组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[ [2,4], [3,4],[2,3],[1,2],[1,3], [1,4]]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
- 1 <= n <= 20
- 1 <= k <= n
思路:
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> resList = new ArrayList<>();
if (n<k || k<1)return resList;
// 从 1 开始是题目的设定
Deque<Integer> path = new ArrayDeque<>();
dfs(n,k,1,path,resList);
return resList;
}
void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> resList){
// 递归终止条件是:path 的长度等于 k
if(path.size()==k){
resList.add(new ArrayList<>(path));
return;
}
// 遍历可能的搜索起点
for (int i=begin;i<=n;i++){
path.addLast(i); // 向路径变量里添加一个数
dfs(n,k,i+1,path,resList); // 下一轮搜索,设置的搜索起点要加 1,因为组合数理不允许出现重复的元素
path.removeLast(); // 重点理解这里:深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
}
}
}
剪枝优化
图中每⼀个节点(图中为矩形),就代表本层的⼀个for循环,那么每⼀层的for循环从第⼆个数开始遍历的话,都没有意义,都是⽆效遍历。
所以,可以剪枝的地⽅就在递归中每⼀层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不⾜
我们需要的元素个数了,那么就没有必要搜索了。
注意代码中i,就是for循环⾥选择的起始位置。
for (int i = startIndex; i <= n; i++)
接下来看⼀下优化过程如下:
- 已经选择的元素个数:
path.size();
- 还需要的元素个数为:
k - path.size();
- 在集合n中⾄多要从该起始位置 :
n - (k - path.size()) + 1
,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是⼀个左闭的集合。
举个例⼦,n = 4,k = 3, ⽬前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
优化后代码
void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> resList){
// 递归终止条件是:path 的长度等于 k
if(path.size()==k){
resList.add(new ArrayList<>(path));
return;
}
// 遍历可能的搜索起点
for (int i=begin;i<=n-(k-path.size())+1;i++){
path.addLast(i); // 向路径变量里添加一个数
dfs(n,k,i+1,path,resList); // 下一轮搜索,设置的搜索起点要加 1,因为组合数理不允许出现重复的元素
path.removeLast(); // 重点理解这里:深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
}
}
- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n)
216、组合总和III
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示:
2 <= k <= 9
1 <= n <= 60
Related Topics
- 数组
- 回溯
思路:
回溯三部曲:
- 确定递归函数参数
List<List<Integer>> resList = new ArrayList<>();
List<Integer> tempList = new ArrayList<>();
public void backTracking(int k, int n,int sum,int startIndex){
n(int)⽬标和。
k(int)就是题⽬中要求k个数的集合。
sum(int)为已经收集的元素的总和,也就是path⾥元素的总和。
startIndex(int)为下⼀层for循环搜索的起始位置。
- 确定终⽌条件
k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。所以如果path.size() 和 k相等了,就终⽌
if (tempList.size()==k){ //1.先满足个数
if (sum==n){ //2.在满足和,才添加
resList.add(new ArrayList<>(tempList));
}
return;
}
- 单层逻辑处理
剪枝优化:
代码如下:
class Solution {
List<List<Integer>> resList = new ArrayList<>();
List<Integer> tempList = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracking(k,n,0,1);
return resList;
}
public void backTracking(int k, int n,int sum,int startIndex){
//4.剪枝操作,当前值 > 目标值就没必要循环下去了
if(sum>n){
return;
}
if (tempList.size()==k){ //1.先满足个数
if (sum==n){ //2.在满足和,才添加
resList.add(new ArrayList<>(tempList));
}
return; // 如果path.size() == k 但sum != n直接返回
}
for (int i=startIndex;i<=9;i++){ //3.startIndex:下一层for循环搜索的起始位置,按照题意也避免重复数字的出现了,之前从1开始到n肯定是错的,读题
sum+=i; // 处理
tempList.add(i); // 处理
backTracking(k,n,sum,i+1); // 注意i+1调整startIndex
sum-=i; // 回溯
tempList.remove(tempList.size()-1); // 回溯
}
}
}