💫目录
回溯算法:
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
回溯法并不高效,顶多可以剪枝优化,本质仍是穷举,是基于递归实现的。
回溯法一般可以解决以下问题:
-
组合问题:N个数里面按一定规则找出k个数的集合
-
排列问题:N个数按一定规则全排列,有几种排列方式
-
切割问题:一个字符串按一定规则有几种切割方式
-
子集问题:一个N个数的集合里有多少符合条件的子集
-
棋盘问题:N皇后,解数独等等
算法模板:
// result 用于存放结果 path 表示路径选择
public void backtrack(参数列表:){
if(满足结束条件){
result.add(路径);
return;
}
fori(遍历所有可能的选择){
if(满足条件){
path.push(); // 处理(添加)节点
} else {
continue;
}
backtrack(参数列表); //回溯
path.pop(); // 撤销处理的结点
}
}
小试牛刀——Leetcode 组合、组合总和问题
1.Leetcode第77题 组合
77. 组合 - 力扣(LeetCode) (leetcode-cn.com)
//给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合
//你可以按 任何顺序 返回答案。(所以 1 3 和 3 1 是一种组合)
public class Leetcode_T77 {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
Deque<Integer> list = new ArrayDeque<>();
backtracking(n,k,list,1);
return res;
}
public void backtracking(int n, int k, Deque<Integer> list, int first){
// 边界
if(list.size() == k){
res.add(new ArrayList<>(list));
return;
}
// i< 这里进行了剪枝
for (int i = first; i <= n - (k - list.size()) + 1; i++) {
list.addLast(i);
backtracking(n,k,list,i + 1);
// 深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
// 比如k = 3, 当找到 1 2 3 时,回退3,变为1 2 ,再继续找 1 2 4
list.removeLast();
}
}
}
2.Leetcode第39题 组合总和
39. 组合总和 - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int n = candidates.length;
if (n == 0) {
return res;
}
// 排序便于后续提前结束循环
Arrays.sort(candidates);
// 用栈来存放每一种组合
Deque<Integer> stack = new LinkedList<>();
dfs(candidates, target, 0, stack);
return res;
}
// index 是每次搜索起点
public void dfs(int[] candidates, int target, int index, Deque<Integer> stack) {
if (target == 0) {
res.add(new ArrayList<>(stack));
return;
}
for (int i = index; i < candidates.length; i++) {
if (target - candidates[i] >= 0) {
stack.addLast(candidates[i]);
dfs(candidates, target - candidates[i], i, stack);
stack.removeLast();
}
}
}
}
3. Leetcode第40题 组合总和||
40. 组合总和 II - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates, target, 0, new ArrayDeque<>());
return res;
}
public void backtracking(int[] candidates, int target, int first, Deque<Integer> stack) {
if (target == 0) {
res.add(new ArrayList<>(stack));
return;
}
for (int i = first; i < candidates.length; i++) {
// 只比39题多这一行代码,遇到重复的continue
if(i > first && candidates[i] == candidates[i - 1]) continue;
if(target - candidates[i] >= 0) {
stack.addLast(candidates[i]);
// first也变为 + 1
backtracking(candidates, target - candidates[i], i + 1, stack);
stack.removeLast();
}
}
}
}
可以看到,这三道题解法基本一样,只有边界递归条件不一样或者具体小细节
第一道是找够个数
第二道却是目标值减小为0并且元素可以重复
第三道也是目标值减小为0但元素不允许重复(遇到重复的跳过即可)
组合总和两道题,先排序可以达到剪枝,因为递增,所以只要遇到大于目标值的,那么后面都不用找了,都不符合
下期更新回溯算法的其他类型题