title: 回溯算法
date: 2022-11-07 10:39:43
tags: LeetCode
概述
一般可以用回溯法解决以下问题
-
组合问题:N个数⾥⾯按⼀定规则找出k个数的集合
-
切割问题:⼀个字符串按⼀定规则有几种切割⽅式
-
子集问题:⼀个N个数的集合⾥有多少符合条件的子集
-
排列问题:N个数按⼀定规则全排列,有几种排列方式
-
棋盘问题:N皇后,解数独
回溯法的问题都可以抽象看为树形结构

for循环可以理解是横向遍历,递归看成纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
77.组合


class Solution {
//存储最后的结果
List<List<Integer>> res = new ArrayList<>();
//存储一个组合
LinkedList<Integer> temp = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
private void backtracking(int n, int k, int start) {
//终止条件 (递归到树形结构的叶子节点)
if (temp.size() == k) {
res.add(new ArrayList<>(temp));
return;
}
for (int i = start; i <= n; i++) {
temp.add(i);
backtracking(n, k, i + 1);
//回溯,撤销已经被处理的结点
temp.removeLast();
}
}
}
对该代码进行剪枝优化
当n,k都等于4时(则返回1~4中所有4个数的组合)此时只需要进行一次遍历就可以获得结果,其他的遍历都是没有意义的

所以可以在递归的每一层for循环里选择起始位置,将代码进行修改
for (int i = startIndex; i <= n; i++)
//改为
for (int i = startIndex; i <= n - (k - temp.size()) + 1; i++)
- temp.size():代表的是,已经选择的元素个数
- k - temp.size():代表的是,还需要的元素个数
- n - (k - temp.size()) + 1:代表的是,在集合n中至多要从该起始位置进行遍历
39. 组合总和

暴力回溯
递归的终止只有两种情况,sum大于target和sum等于target
sum等于target的时候,需要收集结果
注意:进行重复选取

class Solution{
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//用来存储最终结果
List<List<Integer>> res = new ArrayList<>();
//用来存储一个组合
Deque<Integer> temp = new ArrayDeque<>();
backtracking(res, temp,candidates,target,0,0);
return res;
}
/**
* @param start 定义了每层递归的起始位置
*/
private void backtracking(List<List<Integer>> res, Deque<Integer> temp, int[] candidates, int target, int sum, int start) {
int length = candidates.length;
//target 为负数和 0 的时候不再继续向下递归
if (sum > target) {
return;
}
if (sum == target) {
res.add(new ArrayList<>(temp));
return;
}
for (int i = start; i < length; i++) {
//将符合要求的数字加入到组合中
sum += candidates[i];
temp.add(candidates[i]);
/**
* 递归,进行下一层数字的选取
* 此处为i,而不是i+1是为了保证
* 当前元素选取后,下一层仍然有当前元素,以实现重复选取
*/
backtracking(res, temp, candidates, target, sum, i);
//回溯
sum -= candidates[i];
temp.removeLast();
}
}
}
剪枝优化
此时递归结束的判断依据为:sum大于target
但是对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回,其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了
所以,可以对本来的 candidates[] 数组进行排序,排序后,在递归前进行判断,如果下一层的sum(sum + candidates[i])已经大于target,就直接结束for循环遍历

class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//用来存储最终结果
List<List<Integer>> res = new ArrayList<>();
//用来存储一个组合
List<Integer> temp = new ArrayList<>();
//对原本数组进行排序
Arrays.sort(candidates);
backtracking(res, temp,candidates,target,0,0);
return res;
}
/**
* @param start 定义了每层递归的起始位置
*/
private void backtracking(List<List<Integer>> res, List<Integer> temp, int[] candidates, int target, int sum, int start) {
int length = candidates.length;
//当找到了数字之和为target时,即该组合为结果,加入到res中
if (sum == target){
res.add(new ArrayList<>(temp));
return;
}
for (int i = start; i < length; i++) {
//如果新添加的数字大于target,说明不应该加入该数字,终止遍历
if (sum + candidates[i] > target){
break;
}
//将符合要求的数字加入到组合中
temp.add(candidates[i]);
/**
* 递归,进行下一层数字的选取
* 此处为i,而不是i+1是为了保证
* 当前元素选取后,下一层仍然有当前元素,以实现重复选取
*/
backtracking(res, temp, candidates, target, sum + candidates[i], i);
//回溯,移除temp数组中的最后一个元素
temp.remove(temp.size()-1);
}
}
}
分割问题
待补充 \color{#00FF00}{待补充} 待补充
子集问题
待补充 \color{#00FF00}{待补充} 待补充
排列问题
待补充 \color{#00FF00}{待补充} 待补充
棋盘问题
待补充 \color{#00FF00}{待补充} 待补充
其他
待补充 \color{#00FF00}{待补充} 待补充