回溯算法
核心思想
回溯 = 试错 + 撤销。在搜索空间树上 DFS,每一步有多个选择,不满足约束就回退。
典型解题模式:
void backtrack(路径, 选择列表)
if 满足结束条件: 记录结果; return
for 选项 in 选择列表
if 选项非法: continue ← 剪枝
做选择
backtrack(路径, 新选择列表)
撤销选择
子集 (Subsets)
// [1,2,3] → [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> res;
vector<int> path;
function<void(int)> dfs = [&](int start) {
res.push_back(path); // 每个节点都记录
for (int i = start; i < nums.size(); ++i) {
path.push_back(nums[i]);
dfs(i + 1); // i+1:不重复选
path.pop_back();
}
};
dfs(0);
return res;
}
// 复杂度:O(n × 2ⁿ) — 2ⁿ 个子集,每个需要 O(n) 拷贝
排列 (Permutations)
// [1,2,3] → 3! = 6 种排列
vector<vector<int>> permute(vector<int> &nums) {
vector<vector<int>> res;
vector<int> path;
vector<bool> used(nums.size(), false);
function<void()> dfs = [&]() {
if (path.size() == nums.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (used[i]) continue; // 已选过,跳过
used[i] = true;
path.push_back(nums[i]);
dfs();
path.pop_back();
used[i] = false;
}
};
dfs();
return res;
}
// 复杂度:O(n × n!) — n! 个排列
去重排列
// [1,1,2] → [[1,1,2],[1,2,1],[2,1,1]]
// 关键:同层去重
sort(nums.begin(), nums.end());
// 在 for 循环中加:
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
// 解释:相同元素,前一个没用过说明是同层重复
组合 (Combinations)
// C(n, k) — 从 n 个中选 k 个
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
vector<int> path;
function<void(int)> dfs = [&](int start) {
if (path.size() == k) { res.push_back(path); return; }
// 剪枝:剩余不够 k 个
for (int i = start; i <= n - (k - path.size()) + 1; ++i) {
path.push_back(i);
dfs(i + 1);
path.pop_back();
}
};
dfs(1);
return res;
}
N 皇后
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> res;
vector<string> board(n, string(n, '.'));
vector<bool> col(n), diag1(2*n), diag2(2*n); // 列 + 两个对角线
function<void(int)> dfs = [&](int row) {
if (row == n) { res.push_back(board); return; }
for (int c = 0; c < n; ++c) {
if (col[c] || diag1[row+c] || diag2[row-c+n]) continue;
col[c] = diag1[row+c] = diag2[row-c+n] = true;
board[row][c] = 'Q';
dfs(row + 1);
board[row][c] = '.';
col[c] = diag1[row+c] = diag2[row-c+n] = false;
}
};
dfs(0);
return res;
}
// 对角线索引:主对角 row+col 相同,反对角 row-col 相同
剪枝策略总结
| 策略 | 示例 |
|---|---|
| 跳过已用元素 | used[i] |
| 同层去重 | nums[i]==nums[i-1] && !used[i-1] |
| 剩余不够 | n - start + 1 < k - path.size() |
| 提前判断非法 | N 皇后的对角线检查 |
| 排序后剪枝 | 组合总和问题 sort + break |
复杂度快速估算
排列:O(n!)
组合:O(C(n,k))
子集:O(2ⁿ)
N 皇后:O(n!) 但剪枝后远小于