系列文章目录
一、 数组类型解题方法一:二分法
二、数组类型解题方法二:双指针法
三、数组类型解题方法三:滑动窗口
四、数组类型解题方法四:模拟
五、链表篇之链表的基础操作和经典题目
六、哈希表篇之经典题目
七、字符串篇之经典题目
八、字符串篇之 KMP
九、解题方法:双指针
十、栈与队列篇之经典题目
十 一、栈与队列篇之 top-K 问题
十 二、二叉树篇之二叉树的前中后序遍历
十 三、二叉树篇之二叉树的层序遍历及相关题目
十 四、二叉树篇之二叉树的属性相关题目
十 五、 二叉树篇之二叉树的修改与构造
十 六、 二叉树篇之二叉搜索树的属性
十 七、二叉树篇之公共祖先问题
十 八、二叉树篇之二叉搜索树的修改与构造
更新中 … …
前言
刷题路线来自 :代码随想录
组合问题:N个数里面按一定规则找出k个数的集合
回溯算法是一个纯暴力的搜索方法,是树形结构时 DFS (深度优先搜索) 的一种,本文用来解决通过暴力多层循环遍历,因循环的层数变化正常的暴力解无法写出的组合类型问题。同样也是穷举所有结果,但是在这过程中可以进行剪枝。
在回溯算法中通常使用全局变量记录路径和状态。在向下递归的过程中记录的路径和状态,在一条路径递归完,下层递归返回上层递归后,需要进行回溯(将状态回退到进入下层递归之前)。
题录
77. 组合
Leetcode 链接
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。
题解:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
public void backtracking(int n, int k, int start) {
// 结束条件:path.size() == k
if (path.size() == k) {
// 添加到结果集,并返回
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= n; i++) {
path.add(i);
// 因为整数不能重复,下层递归需要从本层递归起始位置 i 加一位置开始
backtracking(n, k, i + 1);
// 回溯,移除本次递归之前添加的值
path.remove(path.size() - 1);
}
}
}
剪枝优化:
已经储存的元素个数:path.size()
还需要找到元素个数:k - path.size()
起始位置至多从:n - (k - path.size()) + 1 开始遍历
...
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
...
}
216. 组合总和 III
Leetcode 链接
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
题解:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int sum = 0;
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k, n, 1);
return res;
}
public void backtracking(int k, int n, int start) {
// 结束条件
if (path.size() > k || sum > n) {
// 和大于 n 或者 收集的元素个数大于 k
return;
}
if (path.size() == k && sum == n) {
// 已经收集到 k 个数,并且和等于 n
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
// 数字不重复,下轮递归从 i + 1 开始
backtracking(k, n, i + 1);
// sum 和 path 都要回溯
path.remove(path.size() - 1);
sum -= i;
}
}
}
不使用全局变量的方式保存 sum ,将 sum 作为递归的参数隐藏回溯,代码更简洁
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
int sum = 0;
backtracking(k, n, 1, sum);
return res;
}
public void backtracking(int k, int n, int start, int sum) {
if (path.size() > k || sum > n) {
return;
}
if (path.size() == k && sum == n) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
//sum += i;
// 将 sum + i 作为参数,传入下层递归
backtracking(k, n, i + 1, sum + i);
path.remove(path.size() - 1);
// 当将 sum + i 作为参数时,本层 sum 并未改变所,回到本层后就不用回溯
//sum -= i;
}
}
}
39. 组合总和
Leetcode 链接
题解:
为了代码简洁 sum 依然放在递归参数中
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates, target, 0, 0);
return res;
}
public void backtracking(int[] nums, int target, int start, int sum) {
if (sum > target) {
return;
}
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
// 一个数可以使用多次,下标 i 用加一
backtracking(nums, target, i, sum + nums[i]);
path.remove(path.size() - 1);
}
}
}
40. 组合总和 II
Leetcode 链接
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
题解:
先排序,可以让相同的数字在一起
因为数组中有重复数字,二返回列表中不允许重复组合的存在如:
数组: nums [1,1,2,3] target = 4
[1,3] 和 [1,3],第二个 1 因为和前一个 1 重复,所以第二个 [1,3] 显然需要剪枝,但是半段条件不能只有 nums [i - 1] = nums [ i ],再看 [1,1,2],在第一个 1 进入第二层递归是这里的第二个 1 是有用的,因为这里的 1 是本层递归的起始位置
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 排序
Arrays.sort(candidates);
backtracking(candidates, target, 0, 0);
return res;
}
public void backtracking(int[] nums, int target, int start, int sum) {
// 此处结束条件放在上层的 for 循环判断条件中了,简洁代码,并减少了递归次数
// if (sum > target) {
// return;
// }
// 结束条件
if (sum == target) {
// 添加并 return
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]);
backtracking(nums, target, i + 1, sum + nums[i]);
// 回溯
path.remove(path.size() - 1);
}
}
}
17. 电话号码的字母组合
Leetcode 链接
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
题解:
- 数组保存数字与字符串的映射关系
- 每层递归的字符串与递归的深度有关,而递归的深度由 digits 的下标决定,所以参数列表需要一个 deep 来记录
- 每次遍历前需要先得到该层递归要遍历的字符串,也就是递归的深度对应的数字的映射字符串
class Solution {
List<String> res = new ArrayList<>();
// 这里的 sb 也可以作为递归的参数,隐藏回溯
StringBuilder sb = new StringBuilder();
public List<String> letterCombinations(String digits) {
if (digits.equals("") || digits == null) {
return res;
}
// 数组保存映射关系
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
backtracking(digits, numString, 0);
return res;
}
public void backtracking(String digits, String[] numString, int deep) {
// 结束条件
if (deep == digits.length()) {
// 添加到结果集并返回
res.add(sb.toString());
return;
}
// 映射数组是从 0 下标开始,digits.charAt(deep) 得到数字的字符形式,
// digits.charAt(deep) - '0' 得到相应下标
String s = numString[digits.charAt(deep) - '0'];
for (int i = 0; i < s.length(); i++) {
sb.append(s.charAt(i));
backtracking(digits, numString, deep + 1);
// 回溯
sb.deleteCharAt(sb.length() - 1);
}
}
}
总结
回溯模板:
public void backtracking(参数) {
if (终止条件) {
// 存放结果并返回
添加到返回列表中;
return;
}
if (其他终止条件) {
// 直接返回,也可以放到 for 循环中判断
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理结点;
backtracking(路径,选择列表);
回溯,撤销处理结果;
}
}
- 返回列表一般作为全局变量,因为简洁,减少递归的参数列表
- 类似 sum、字符串等需要在递归过程中动态维护,并回溯的,除作为全局变量外,也可以作为递归的参数,隐藏回溯。
- 剪枝通常修改 for 循环结束条件类似上边几道题