回溯part011
今天学习了回溯算法:
- 基本知识,关键是那个模板
- 组合问题:画树状图+简单的剪枝
- 电话号码的组合问题,和经典组合问题的差别在于取不同集合中的组合,注意如何有限制的在for循环之前确定循环哪个数组,通过树状图确定for循环中i的大小。
1. 基本知识
-
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。
-
本质是穷举,穷举所有可能,然后选出我们想要的答案,所以效率不高。
-
解决的问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
-
模板:
回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
2. 77 组合
题目:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
a. 如何想到回溯
直观的方法,k=2就是两层循环,k=50就是50层循环嵌套。
回溯可以解决很多层for循环嵌套问题。
回溯问题可以抽象为树形结构,如图:

- 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
- 图中可以发现n相当于树的宽度,k相当于树的深度。
- 图中每次搜索到了叶子节点,我们就找到了一个结果。
b. 回溯三部曲
- 递归函数的返回值以及参数
在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
vector<vector<int>> result; // 存放符合条件结果的集合 vector<int> path; // 用来存放符合条件结果
函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数。
然后还需要一个参数,为int型变量startIndex
,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] ),防止出现重复的组合。
- 回溯函数终止条件
什么时候到达所谓的叶子节点了呢?path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。
- 单层搜索的过程
for循环每次从startIndex
开始遍历,然后用path保存取到的节点i。然后backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。backtracking的下面就是回溯的操作,撤销本次处理的结果。
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) { result.clear(); // 可以不写 path.clear(); // 可以不写 backtracking(n, k, 1); return result; } };
-
时间复杂度: O(n * 2^n)
对于每一个
n
,都有两种选择:要么被选中加入到path
中,要么不被选中。因此,对于n
个元素,理论上最多有2^n
种选择方式。最多k=n,所以再乘一个n。 -
空间复杂度: O(n)
c. 剪枝优化
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。

优化过程如下:
- 已经选择的元素个数:
path.size()
; - 所需需要的元素个数为:
k - path.size()
; - 列表中剩余元素(
n-i) >= 所需需要的元素个数(k - path.size())
- 在集合n中至多要从该起始位置 :
i <= n - (k - path.size()) + 1
,开始遍历
为什么有个+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++) // i为本次搜索的起始位置
3. 316 组合Ⅲ
题目:找出所有相加之和为 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]]
和上一题的组合异曲同工,需要注意的地方:不是所有叶子节点都要,只要sum=target的结果。所以这个判断在代码的if中。

class Solution { private: vector<vector<int>> result; // 存放结果集 vector<int> path; // 符合条件的结果 void backtracking(int targetSum, int k, int sum, int startIndex) { if (path.size() == k) { if (sum == targetSum) result.push_back(path); return; // 如果path.size() == k 但sum != targetSum 直接返回 } 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; } };
剪枝的方法:
已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。
class Solution { private: vector<vector<int>> result; // 存放结果集 vector<int> path; // 符合条件的结果 void backtracking(int targetSum, int k, int sum, int startIndex) { if (sum > targetSum) { // 剪枝操作 return; } if (path.size() == k) { if (sum == targetSum) result.push_back(path); return; // 如果path.size() == k 但sum != targetSum 直接返回 } for (int i = startIndex; i <= 9 - (k - path.size()) + 1; 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; } };
- 注意剪枝的两个地方:sum和k
- 有了sum,那么每次递归和回溯需要修改sum的值,不是只有path了。
4. 17.电话号码的字母组合
题目:给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
要解决如下三个问题:
-
数字和字母如何映射
-
两个字母就两个for循环,三个字符就三个for循环,明显是多层for循环问题,可以用回溯解决,属于回溯中的组合问题,和经典组合不同的地方在于求不同集合之间的组合,所以每个for循环需要在固定的个数数字中挑一个,画出树状图:
-
输入1 * #按键等等异常情况
a. 数字和字母如何映射
映射问题,可以使用map或者定义一个二维数组,例如:string letterMap[10]
,定义一个二维数组,代码如下:
const string letterMap[10] = { "", // 0 "", // 1 "abc", // 2 "def", // 3 "ghi", // 4 "jkl", // 5 "mno", // 6 "pqrs", // 7 "tuv", // 8 "wxyz", // 9 };
b. 回溯法解决n个for循环的问题
- 确定回溯函数参数
全局:字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来。
参数:参数指定是有题目中给的string digits
,然后还要有一个参数就是int型的index。
注意这个index可不是 前两道题中的startIndex
了,这个index是记录遍历第几个数字了,即用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。
- 确定终止条件
例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果index 等于 输入的数字个数(digits.size)
了(本来index就是用来遍历digits的)。然后收集结果,结束本层递归。
- 确定单层遍历逻辑
首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。然后for循环来处理这个字符集,for循环中递归调用和回溯。
class Solution { private: const string letterMap[10] = { "", // 0 "", // 1 "abc", // 2 "def", // 3 "ghi", // 4 "jkl", // 5 "mno", // 6 "pqrs", // 7 "tuv", // 8 "wxyz", // 9 }; public: vector<string> result; string s; void backtracking(const string& digits, int index) { if (index == digits.size()) { result.push_back(s); return; } int digit = digits[index] - '0'; // 将index指向的数字转为int string letters = letterMap[digit]; // 取数字对应的字符集 for (int i = 0; i < letters.size(); i++) { s.push_back(letters[i]); // 处理 backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了 s.pop_back(); // 回溯 } } vector<string> letterCombinations(string digits) { s.clear(); result.clear(); if (digits.size() == 0) { return result; } backtracking(digits, 0); return result; } };
- 二位数组可以做到数字和字母映射
- 递归回溯问题,可以试试画树状图,从而可以看出for循环中循环多少次
- 此题中,index很重要,在for循环前,通过index确定取的是二维数组中哪个数字对应的字母
- 注意此题空间复杂度,递归问题的空间复杂度和递归层数有关系
今日古诗
八声甘州·记玉关踏雪事清游
张炎〔宋代〕
辛卯岁,沈尧道同余北归,各处杭、越。逾岁,尧道来问寂寞,语笑数日。又复别去。赋此曲,并寄赵学舟。
记玉关踏雪事清游,寒气脆貂裘。傍枯林古道,长河饮马,此意悠悠。短梦依然江表,老泪洒西州。一字无题处,落叶都愁。
载取白云归去,问谁留楚佩,弄影中洲?折芦花赠远,零落一身秋。向寻常、野桥流水,待招来,不是旧沙鸥。空怀感,有斜阳处,却怕登楼。
这首词上片以“记”字领起,气势较为开阔、笔力劲峭。写词人前年冬季赴北写经的旧事,展现了一幅冲风踏雪的北国羁旅图;旧事重提之后,中片则续写北地回归之光景;下片从眼前的离别写起,表现出零落如秋叶的心情。全词先悲后壮,先友情而后国恨,惯穿始终的,是一股荡气回肠的“词气”,词中写身世飘萍和国事之悲感哀婉动人,令人如闻断雁惊风、哀猿啼月。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现