carl算法学习日记
转载至:微信公众号:代码随想录
一.回溯算法初探
1.什么是回溯法
回溯算法是一种暴力的枚举算法。二叉树的递归算法即是回溯算法。递归和回溯指的是同一个东西。
2.回溯算法效率
回溯本质上是枚举算法,并不高效。某些问题之所以使用回溯算法是因为没得选,只能进行暴力求解。最多进行剪枝优化。
3.回溯算法解决的问题
回溯法,一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
排列问题:N个数按一定规则全排列,有几种排列方式
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
棋盘问题:N皇后,解数独等等
4.如何理解回溯算法
回溯算法都可以抽象为树形结构。
回溯算法在集合中递归查找子集,因此集合大小就是树的宽度,递归深度就是树的深度。
搜素到叶子节点即终止,因此此树必然高度有限。
5.模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
二.回溯求组合问题
1.题目
题目链接:leetcode
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
2.思路
对于本问题,首先想到可以使用for循环求解。如:
int i=4;
for(int i = 1;i<=4;i++){
for(int j = i+1;j<=4;j++){
cout<<i<<" "<<j<<endl;
}
}
但是因为k是变化的,必然要考虑使用递归进行求解。
转化为树形结构,结果集逐渐缩小。如下:
其中,n便是树的宽度,k便是树的深度。
解题思路:
递归终止条件:达到叶子节点。
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,通过传参进行调整
3.代码
首先定义全局变量存放中间结果和最终结果。
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
确定变量传递:
这里要传递的变量只用来确定开始范围。n和k是最开始的条件,当然这两个条件也可以定义为全局变量从而简化参数:
void backtracking(int n, int k, int startIndex);
int k,n;//全局变量,记得要初始化
void backtracking(int startIndex);//简化传参
终止条件:遇到叶子节点即终止。这里指的是path中存放的数字数目等于k时,就可以终止并保存了。
if (path.size() == k) {
result.push_back(path);//保存结果
return;
}
递归入口:
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 处理节点,加入路径
backtracking(n, k, i + 1); // 递归
path.pop_back(); // 回溯,撤销处理的节点
}
最终代码:
class Solution {
private:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n; i++)
{
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
4.剪枝优化
在递归遍历中,某些遍历是没有意义的。举个极端的例子。如下:
当n = 4,k = 4,时集合只有一个解[1,2,3,4]。但是在第一层循环中,去掉1后,集合中剩下[2,3,4],显然不可能再有符合k = 4的解。所以可以通过优化for循环条件来去除多余的循环和递归。
优化前:
for (int i = startIndex; i <= n; i++)
优化思路:
1.已经选择的元素个数:path.size();
2.还需要的元素个数为: k - path.size();
3.在集合n中至少要从该起始位置 : n - (k - path.size()) + 1,开始遍历。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
优化后:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)
代码
class Solution {
private:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)
{
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
题目:https://leetcode-cn.com/problems/combinations/submissions/
优化前:执行时间为44,44ms
优化后:执行时间为16,20ms
三.求组合总和问题
1.题目
链接:https://leetcode-cn.com/problems/combination-sum-iii/
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
2.思路
本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。和上道题相比,不仅有个数k的限制,还有总和n的限制。和上一道题的变化仅在于终止条件的变化。
3.代码
终止条件,满足k个数,并且总和为n:
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return;
}
其余部分和上题大致类似。
class Solution {
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
// targetSum:目标和,也就是题目中的n。
// k:题目中要求k个数的集合。
// sum:已经收集的元素的总和,也就是path里元素的总和。
// startIndex:下一层for循环搜索的起始位置。
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (sum > targetSum) { // 剪枝操作
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return;
}
for (int i = startIndex; i <= 9; i++) {
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
}
};
4.剪枝优化
这道题求总和,那么当sum大于n时,直接返回。
//已选的元素总和大于n
if (sum > n) {
return;
}
四.电话号码组合问题
1.题目
地址:https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/
示例:
输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
2.思路
这里每个数字对应了一个字母集合。