++回溯算法(backtracking algorithm)++是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。
回溯算法通常采用"深度优先搜索"来遍历解空间。在"二叉树"章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。
之所以称之为回溯算法,是因为该算法在搜索解空间时会采用"尝试"与"回退"的策略。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。
在每次"尝试"中,我们通过将当前节点添加进 path
来记录路径;而在"回退"前,我们需要将该节点从 path
中弹出,以恢复本次尝试之前的状态。
复杂的回溯问题通常包含一个或多个约束条件,约束条件通常可用于"剪枝"
77组合
题目描述
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
代码:
cpp
class Solution {
public:
// 用于存储最终的组合结果
vector<vector<int>> result;
// 用于存储当前组合的路径
vector<int> path;
// 回溯函数
// n: 数组的最大值
// k: 组合中数字的数量
// startindex: 当前递归的起始索引
void backtracking(int n,int k,int startindex){
// 如果当前路径的长度等于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(); // 回溯,移除当前数字,尝试下一个数字
}
}
// 主函数,用于生成组合
vector<vector<int>> combine(int n, int k) {
path.clear(); // 清空路径
result.clear(); // 清空结果
backtracking(n, k, 1); // 从索引1开始回溯
return result; // 返回最终结果
}
};
回溯算法其基本思想是:以某种方式构建解决方案,一旦确定某步无法继续构造出合法答案,就立即返回步进一步试其他可能性。
-
主函数
combine
:- 初始化存储结果的容器
result
和当前路径path
。 - 开始从索引
1
进行回溯构建组合。
- 初始化存储结果的容器
-
回溯函数
backtracking
:- 结束条件 :如果当前路径长度等于组合要求的长度
k
,将当前路径加入结果result
中。 - 循环构建组合 :
- 从
startindex
循环到n
,每次选择一个数字加入路径path
。 - 继续递归构建下一个位置的候选数字,调用
backtracking
,但起始索引是i + 1
,这样就避免了重复选择数字。 - 回溯:尝试完一个候选组合后,将最后加入的元素从路径中移除,尝试下一个可能的数字。
- 从
- 结束条件 :如果当前路径长度等于组合要求的长度
递归回溯的抽象解释:
- 构建:每次选择一个数字加入当前路径,并尝试继续构建(递归)。
- 退出条件:当路径长度达到要求时,返回并保存结果。
- 回溯:如果构建过程中发现无法继续构建有效路径,就撤销最后一个选择(回溯),尝试其他可能性。
这个过程确保了通过所有可能的候选解决方案,构建出所有满足条件的组合。
216组合总和(与组合基本一摸一样)
题目描述
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
代码:(锁了一个剪枝操作)
cpp
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (sum > targetSum) {
}
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return;
}
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
sum += i;
path.push_back(i);
backtracking(targetSum, k, sum, i + 1);
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;
}
};
17电话字母的组合
题目描述
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
代码:
cpp
class Solution {
public:
// 声明一个数组,存储数字对应的字母映射
string letterMap[10] = {
"", // 0 对应无字母
"", // 1 对应无字母
"abc", // 2 对应abc
"def", // 3 对应def
"ghi", // 4 对应ghi
"jkl", // 5 对应jkl
"mno", // 6 对应mno
"pqrs", // 7 对应pqrs
"tuv", // 8 对应tuv
"wxyz", // 9 对应wxyz
};
vector<string> result; // 用来存放结果的组合
string path; // 用来存放当前路径
// 回溯函数
// digits: 输入的数字字符串
// index: 当前递归到的数字位置
void backtracking(string digits, int index) {
// 如果当前路径长度等于输入的数字字符串长度
if (index == digits.size()) {
result.push_back(path); // 将当前路径加入结果
return;
}
// 获取当前数字对应的字母集
int num = digits[index] - '0'; // 将字符转换为数字
string letter = letterMap[num]; // 字母映射
// 遍历当前数字对应的每个字母
for (int i = 0; i < letter.size(); i++) {
path.push_back(letter[i]); // 将字母加入当前路径
backtracking(digits, index + 1); // 递归调用,处理下一个数字
path.pop_back(); // 回溯,移除当前字母,尝试下一个可能的字母
}
}
// 主函数,用于产生所有可能的字母组合
vector<string> letterCombinations(string digits) {
result.clear(); // 清空结果
path.clear(); // 清空当前路径
if (digits.size() == 0) {
return result; // 如果输入为空,直接返回空结果
}
backtracking(digits, 0); // 从索引0开始回溯
return result; // 返回最终结果
}
};
解释
该代码旨在解决给定手机号码输入(即按键数字),返回所有可能的字母组合的任务。通过回溯算法逐个构建可能的字母组合,并在递归过程中进行回溯和尝试其他可能性。以下是详细解释:
-
数据结构和变量:
letterMap
:一个数组,用于存储数字与字母的映射关系,例如数字2对应abc
,数字3对应def
等。result
:用于存储所有可能的字母组合。path
:用于在回溯时,记录当前的组合路径。
-
主要函数
letterCombinations
:- 清空
result
和path
,确保函数每次调用时都是干净的开始。 - 如果输入的数字字符串为空,直接返回空结果。
- 启动回溯函数
backtracking
,从索引0开始。 - 返回最终结果
result
。
- 清空
-
回溯函数
backtracking
:- 结束条件:如果当前路径长度等于输入数字字符串的长度,说明找到一个有效组合,将其加入结果中并返回。
- 获取当前索引处的数字,并查询其在
letterMap
中的字母映射。 - 递归与回溯 :
- 遍历当前数字对应的每个字母。
- 将字母加入当前路径
path
。 - 递归调用函数处理下一个数字。
- 回溯:递归调用返回后,把当前字母移除,尝试下一个可能的字母。
递归回溯的抽象解释
递归回溯的过程可以形象地理解为"探索和撤退"。类似于树状结构的路径探索,每个节点代表一个选择(一个字母),每次递归深入一层,尝试新的选择,直到路径长度符合要求(达到输入数字串的长度)。
- 构建路径:在回溯树的每一个分支上,加入当前字母,继续递归。
- 回溯:到达叶节点(路径长度与输入数字串长度相同)时,将路径加入结果,否则撤销最后一步的选择(从路径中删除字母),返回上一级尝试下一个可能的选择。
这一过程既保证了所有可能的组合都会被尝试,也确保一旦当前路径无法再延续时,会及时撤销选择,避免无效的继续探索。
以下取自hello算法
13.1.4 常用术语¶
为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例,如表 13-1 所示。
表 13-1 常见的回溯算法术语
名词 | 定义 | 例题三 |
---|---|---|
解(solution) | 解是满足问题特定条件的答案,可能有一个或多个 | 根节点到节点 7 的满足约束条件的所有路径 |
约束条件(constraint) | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 路径中不包含节点 3 |
状态(state) | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 path 节点列表 |
尝试(attempt) | 尝试是根据可用选择来探索解空间的过程,包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 path ,判断节点的值是否为 7 |
回退(backtracking) | 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶节点、结束节点访问、遇到值为 3 的节点时终止搜索,函数返回 |
剪枝(pruning) | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 3 的节点时,则不再继续搜索 |