目录
引言
今天开始回溯算法篇章,首先总结一下回溯算法的核心思想和解决的问题类型。然后再写全排列的算法求解。
回溯算法
回溯算法的核心思想
回溯算法是一种通过 递归试探 和 撤销选择(回退) 来搜索所有可能解的暴力穷举算法。其核心思想是:
- 逐步构建候选解:每一步选择一个可能的路径,进入下一层决策。
- 约束条件剪枝:如果当前路径不满足条件(如冲突、越界),则立即回溯,不再继续搜索。
- 撤销选择:当发现当前路径无法得到解时,回退到上一步,尝试其他选项。
关键特点:
- 递归实现(隐式调用栈)或迭代实现(显式栈)。
- 通过剪枝(Pruning)减少无效搜索,提高效率。
回溯算法解决的问题类型
回溯通常用于解决以下问题:
-
组合问题
-
排列问题
-
子集问题
-
棋盘/路径问题
-
分割问题
回溯算法的通用模板(递归实现)
void backtrack(路径, 选择列表, 结果集) {
if (满足结束条件) {
结果集.add(路径);
return;
}
for (选择 : 选择列表) {
if (不满足约束条件) continue; // 剪枝
做选择; // 将选择加入路径
backtrack(路径, 新选择列表, 结果集); // 递归
撤销选择; // 回溯,恢复状态
}
}
回溯 vs. 动态规划(DP)
特性 | 回溯算法 | 动态规划 |
---|---|---|
适用问题 | 求所有解、组合/排列问题 | 求最优解、计数问题 |
时间复杂度 | 通常指数级(如 O(2^n) | 通常多项式级(如 O(n^2)) |
空间复杂度 | 取决于递归深度(O(n)) | 通常需要 DP 表(O(n) 或 O(n^2)) |
是否记录中间状态 | 不记录,重复计算可能发生 | 记录子问题结果,避免重复计算 |
优化回溯的方法
- 剪枝(Pruning):提前跳过不符合条件的分支(如组合总和问题中排序后提前终止)。
- 记忆化搜索:结合哈希表记录中间状态,避免重复计算(如含重复元素的排列问题)。
- 迭代实现:用显式栈替代递归,防止栈溢出(如 DFS 的非递归写法)。
总结
回溯算法是解决 组合、排列、子集、棋盘类问题 的利器,通过递归和剪枝平衡穷举与效率。掌握其模板和适用场景后,可灵活应对大多数暴力搜索问题。
全排列
- 🎈 题目链接:
- 🎈 做题状态:
我的解题
基本思路:将全排列问题抽象为一颗决策树,第一层选择一个数字,第二层选择第二个数字,直到当前路径长度等于 nums.size() 就可以退出函数。为了记录哪些元素被选择过,还需要使用一个额外的空间来记录。
class Solution {
public:
// 回溯全排列
void tracback(vector<vector<int>>& res, vector<int>& path, vector<int>& nums, vector<bool>& used)
{
// 终止条件
if (path.size() == nums.size())
{
res.push_back(path);
return;
}
// 遍历当前层
for (int i = 0; i < nums.size(); ++i)
{
// 判断当前元素是否使用过,如果使用过就跳过
if (used[i]) continue;
path.push_back(nums[i]);
used[i] = true;
tracback(res, path, nums, used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
vector<bool> used(nums.size(), false);
tracback(res, path, nums, used);
return res;
}
};
代码优化
✅ 优点分析:
- 代码结构清晰:
permute()
是主函数,tracback()
是递归函数,职责分明。 - 逻辑严谨:通过
used
数组判断元素是否使用,避免重复选取。 - 剪枝到位:
if (used[i]) continue;
是标准的剪枝方式。 - 回溯操作规范:先选择、再递归、再撤销选择。
🧠 可优化/改进建议:
1. ✅ 拼写错误(小问题)
函数名 tracback
建议改成 backtrack
,拼写更规范,便于阅读。
// void tracback(...) → void backtrack(...)
2. ✅ 函数传参优化
你现在传的是 vector<int>& nums
,这个本身已经是引用了,但因为函数中不修改 nums
,你可以改为 const vector<int>& nums
,表达不修改数据的意图,增强可读性:
void backtrack(vector<vector<int>>& res, vector<int>& path, const vector<int>& nums, vector<bool>& used)
3. ✅ 使用 emplace_back 替换 push_back(略微提升性能)
在 C++ 中,emplace_back()
可以避免不必要的复制构造。
path.emplace_back(nums[i]);
// 和
res.emplace_back(path);
4. ✅ 减少不必要的传值
虽然 STL 中传引用性能很好,但 vector<vector<int>> &res
其实可以考虑在递归时省略传入(作为类成员变量),不过这属于风格偏好,在刷题中不是必须优化。
🔍 完整优化后的代码(含命名修改):
class Solution {
public:
void backtrack(vector<vector<int>>& res, vector<int>& path, const vector<int>& nums, vector<bool>& used) {
if (path.size() == nums.size()) {
res.emplace_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (used[i]) continue;
path.emplace_back(nums[i]);
used[i] = true;
backtrack(res, path, nums, used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
vector<bool> used(nums.size(), false);
backtrack(res, path, nums, used);
return res;
}
};