代码随想录算法训练营第19天|回溯算法理论基础、77. 组合、组合优化、216.组合总和III、17.电话号码的字母组合
回溯算法理论基础
2025-02-19 10:32:52 星期三
文档讲解:代码随想录(programmercarl)回溯算法理论基础
视频讲解:《代码随想录》算法视频公开课:带你学透回溯算法(理论篇)
代码随想录视频内容简记
关于回溯
回溯问题实际上就是纯暴力搜索,不是特别高深的算法,其适用范围多是针对暴力无法解决,或者没办法写出代码的一些问题。
-
组合问题
-
切割问题
-
子集问题
-
排列问题
-
棋盘问题
回溯算法可以抽象为一棵N叉树,树的宽度表示了集合的大小,需要用for进行遍历,而树的深度则由递归的深度来构成
回溯法怎么用
回溯有模板
void (参数) {
if (终止条件) {
存放结果
return
}
for (集合大小) {
处理结点
递归函数
回溯操作(进行撤销)
}
}
LeetCode77
题目描述:力扣77
文档讲解:代码随想录(programmercarl)77. 组合
视频讲解:《代码随想录》算法视频公开课:带你学透回溯算法-组合问题(对应力扣题目:77.组合)
代码随想录视频内容简记
回溯的三部曲和递归三部曲是一样的
梳理
-
确定函数的参数和返回值。
-
确定递归的终止条件
-
确定单层递归的逻辑
大致代码内容
-
首先是确定一个路径变量
result<int> path
,一个结果集vector<vector<int>> result
定义一个没有返回值的函数void backtracking(n, k, index)
。 -
确定递归的终止条件,
if (path.size() == k) result.push_back(path); return;
-
确定单层递归的逻辑。
for (int i = index; i <= n; i++) path.push_back(i);
这里就是对结点的处理,向末尾添加元素。backtracking(n, k, i + 1);
递归函数,注意,这里的i + 1作用就是让进入下一次递归的index参数赋值为i + 1
,那么下一个for循环i的起始就会被赋值为index
,防止再取相同的元素,即自己本身,因为本题是组合问题,所以不同排列的结果是一致的。path.pop_back(i);
最后一步就是回溯的撤销
LeetCode测试
点击查看代码
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int index) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = index; 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;
}
};
LeetCode77优化
视频讲解:组合问题的剪枝操作
代码随想录视频内容简记
关于77组合问题的剪枝优化,其实就是针对在对集合进行遍历的过程中,永远无法满足k大小要求的一些结果做删除。比如,输入n = 4, k = 4
,那么在取1之后,剩余的2,3,4;取2之后,剩余的3,4
;取3之后,剩余的4
;取4之后,剩余的空。可见,剩余的后3种情况,都无法满足k = 4
的要求,所以剪枝。

在这里,剪枝的核心就是对循环的终止条件做优化。我们知道,在每次取完一个i之后,剩余可供取的元素个数就会变成n - i
(为了防止重复取),那么,能确保k的要求被满足的条件就是:可供取的元素个数一定要大于等于当前需要取的元素
for (int i = index; i <= 剪枝; i++) {
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
在本题中,已经放入path的元素数量为path.size()
,那么待添加的元素(需要取的)数量为k - path.size()
,一旦出现,那么也就是,又因为i的取值包括其本身,所以这里应该+1。其实这里不是特别清楚明白,但是姑且按照这么来理解。
后记:做到216的时候,k哥画了一个图易于理解这里

因为剩余算出来的n - (k - path.size())是还剩下的元素,是一个个数,要想让i指向那里,必须让i+1
LeetCode测试
点击查看代码
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int index) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = index; 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;
}
};
LeetCode216
题目描述:力扣216
文档讲解:代码随想录(programmercarl)216.组合总和III
视频讲解:《代码随想录》算法视频公开课:和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III
代码随想录视频内容简记
组合总和的思路和组合也是一样的,区别就是多了一个sum的限制。而针对于这个sum,一开始自己先写代码的时候,有一个疑问,就是到底sum的和,还有path的大小,哪一个才是递归的终止条件呢?
事实上,两个都是,path.size() == k && sum == n
,而且sum会多出来一个剪枝,这里要注意
梳理
依旧是递归(回溯)三部曲
-
确定函数的参数和返回值
-
确定递归的终止条件
-
确定单层递归的逻辑
大致代码内容
-
确定函数的参数和返回值
void backtracking(int k, ink n, int sum, int index)
这里会多出来一个参数sum -
确定递归的终止条件,
if (path.size() == k && sum == n) result.push_back(path); return;
这里和77是一摸一样的 -
确定单层递归的逻辑。这里和77也没有什么变化,唯一的区别就是在对于递归的时候多传递一个参数
剪枝优化
对sum做剪枝
sum的剪枝,就是一旦sum大于了题目要求的和n,那么就直接返回
if (sum > n) return
对i做剪枝
和昨天的思路一样,对i的条件判断做剪枝,具体就是i < 9 - (k - path.size()) + 1
LeetCode测试
好久没有看时间复杂度了,这个是,😶不太明白
未剪枝
点击查看代码
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
int sum;
void backtracking(int k, int n, int sum, int index) {
if (path.size() == k && sum == n) {
result.push_back(path);
return;
}
for (int i = index; i <= 9; i++) {
sum += i;
path.push_back(i);
backtracking(k, n, sum, i + 1);
sum -= i;
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 0, 1);
return result;
}
};
剪枝
点击查看代码
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
int sum;
void backtracking(int k, int n, int sum, int index) {
if (sum > n) return;
if (path.size() == k && sum == n) {
result.push_back(path);
return;
}
for (int i = index; i <= 9 - (k - path.size()) + 1; i++) {
sum += i;
path.push_back(i);
backtracking(k, n, sum, i + 1);
sum -= i;
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 0, 1);
return result;
}
};
LeetCode17
题目描述:力扣17
文档讲解:代码随想录(programmercarl)17.电话号码的字母组合
视频讲解:《代码随想录》算法视频公开课:还得用回溯算法!| LeetCode:17.电话号码的字母组合
代码随想录视频内容简记
首先,17题是一个两个集合的组合问题,这也是他和77还有216不同的地方,一次只在一个集合中去一个数。
另外需要注意的一点是本题递归的终止条件,回看77和216可以发现,递归的深度是和递归的终止条件紧紧相关的。77当中的k控制了path的大小,也就是递归的深度,216当中的k也同样控制path的大小(递归的深度),在本题中,因为一次只在一个集合中取一个元素,那么,集合的个数就控制了递归的深度。k哥用index来表示对集合的遍历
注意在做回溯题的时候,画出他的树形图
很重要,很多就一目了然了
梳理
-
确定函数的参数和返回值
-
确定递归的终止条件
-
确定单层递归的逻辑
大致代码内容
-
在本题中,需要做一个数字到字母的映射,用map。另外就是
void backtracking(string digits, int index)
,int digit = digits[index] - '0'
用index指向每次需要取字符的数字(每个集合的索引),并-'0'表示转换成整形。之后可以用digit取出对应集合的字母元素string letter = letterMap[digit]
。还要注意的就是对
string path
和vector<string> result
的定义,result仍然是一个二维的,因为里面的string是字符串 -
确定递归的终止条件,
if (index == digits.size()) result.push_back(path); return;
-
确定单层递归的逻辑,
for (int i = 0; i < letter.size(); i++)
,这里和前面的题显著不一样的地方就是int i = 0
,而不是startIndex,因为这里不再是一个集合,不用去重。之后path.push_back(letter[i]);
backtracking(digits, index + 1)
,注意这里的index + 1
就很好地表示了直接进入下一个集合。path.pop_back();
LeetCode测试
感觉整个题最巧妙的地方就是每次递归,用index取digit,再用letter表示为字母的集合,然后就在递归中进行push和pop
在测试的时候出现了这种情况,就是因为index初始为0,而又因为digits.size()=0
,相当于会添加一个空的path,也就是下图的两个双引号,我看了一下k哥给的是在主函数直接加了一个判断

完整代码如下
点击查看代码
class Solution {
private:
string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> result;
string path;
void backtracking(string digits, int index) {
if (index == digits.size()) {
result.push_back(path);
return;
}
int digit = digits[index] - '0';
string letter = letterMap[digit];
for (int i = 0; i < letter.size(); i++) {
path.push_back(letter[i]);
backtracking(digits, index + 1);
path.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) return result;
backtracking(digits, 0);
return result;
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端