回溯算法是解决组合优化问题的利器,本文将从基础概念讲起,逐步深入,通过经典例题带你掌握回溯算法的核心思想和实现技巧。
什么是回溯算法?
回溯算法(Backtracking)是一种通过探索所有可能的候选解来找出所有解的算法。当候选解被确认不是一个可行解(或者不是最优解)时,回溯算法会撤销上一步的操作,尝试其他的选择。
回溯法通常用于解决以下类型的问题:
-
组合问题(如全排列、子集)
-
约束满足问题(如N皇后、数独)
-
切割问题(如分割回文串)
-
棋盘问题(如骑士巡游)
回溯算法的核心思想
回溯算法的核心在于"试错"思想:从一条路往前走,能进则进,不能进则退回来,换一条路再试。这种思想可以用树形结构表示:
-
路径:已经做出的选择
-
选择列表:当前可以做的选择
-
结束条件:到达决策树底层,无法再做选择
回溯算法的通用框架
void backtracking(参数) {
if (满足结束条件) {
存放结果;
return;
}
for (选择 : 本层选择列表) {
处理当前选择;
backtracking(路径,选择列表); // 递归
撤销选择; // 回溯关键步骤
}
}
经典例题1:全排列问题
问题描述
给定一个不含重复数字的数组 nums
,返回其所有可能的全排列。
示例:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
解题思路
-
使用一个布尔数组
used
记录每个元素是否被使用过 -
递归过程中,将未使用的元素加入当前路径
-
当路径长度等于数组长度时,保存结果
-
递归结束后撤销选择,回溯到上一步
C++代码实现
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
vector<bool> used(nums.size(), false);
backtracking(nums, res, path, used);
return res;
}
private:
void backtracking(vector<int>& nums, vector<vector<int>>& res,
vector<int>& path, 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; // 跳过已使用的元素
// 做选择
used[i] = true;
path.push_back(nums[i]);
// 递归进入下一层决策树
backtracking(nums, res, path, used);
// 撤销选择(回溯)
path.pop_back();
used[i] = false;
}
}
};
// 测试代码
int main() {
Solution solution;
vector<int> nums = {1, 2, 3};
vector<vector<int>> res = solution.permute(nums);
cout << "全排列结果:" << endl;
for (auto& vec : res) {
for (int num : vec) {
cout << num << " ";
}
cout << endl;
}
return 0;
}
复杂度分析
-
时间复杂度:O(n × n!),其中 n 为数组长度
-
空间复杂度:O(n),递归调用栈的最大深度为 n
经典例题2:N皇后问题
问题描述
在 n×n 的棋盘上放置 n 个皇后,使其不能互相攻击(任意两个皇后不能处于同一行、同一列或同一斜线上)。返回所有解决方案。
示例(4皇后问题):
输入:4
输出:[
[".Q..",
"...Q",
"Q...",
"..Q."],
["..Q.",
"Q...",
"...Q",
".Q.."]
]
解题思路
-
按行放置皇后,避免行冲突
-
检查当前列是否已被占用
-
检查两条对角线(左上到右下,右上到左下)是否已被占用
-
使用递归尝试每一行的所有列位置
C++代码实现
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> res;
vector<string> board(n, string(n, '.')); // 初始化棋盘
backtracking(0, n, board, res);
return res;
}
private:
void backtracking(int row, int n, vector<string>& board,
vector<vector<string>>& res) {
// 终止条件:所有行都放置完毕
if (row == n) {
res.push_back(board);
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, board, n)) {
board[row][col] = 'Q'; // 放置皇后
backtracking(row + 1, n, board, res); // 递归下一行
board[row][col] = '.'; // 回溯,撤销皇后
}
}
}
bool isValid(int row, int col, vector<string>& board, int n) {
// 检查同一列
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') return false;
}
// 检查左上对角线
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') return false;
}
// 检查右上对角线
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q') return false;
}
return true;
}
};
// 测试代码
int main() {
Solution solution;
int n = 4;
vector<vector<string>> res = solution.solveNQueens(n);
cout << n << "皇后问题的解:" << endl;
for (int i = 0; i < res.size(); i++) {
cout << "解法 " << i + 1 << ":" << endl;
for (string row : res[i]) {
cout << row << endl;
}
cout << endl;
}
return 0;
}
复杂度分析
-
时间复杂度:O(n!),最坏情况下需要尝试所有排列组合
-
空间复杂度:O(n²),存储棋盘状态
回溯算法优化:剪枝技巧
剪枝是回溯算法中最重要的优化手段,通过提前排除不可能的解,减少递归次数:
-
可行性剪枝:在递归前检查当前选择是否满足约束条件
-
最优性剪枝:当当前路径已不可能优于已有最优解时终止
-
对称性剪枝:避免重复计算对称解
剪枝示例(全排列问题优化)
// 优化:减少used数组的访问次数
void backtracking(vector<int>& nums, vector<vector<int>>& res,
vector<int>& path, vector<bool>& used) {
if (path.size() == nums.size()) {
res.push_back(path);
return;
}
// 使用局部变量避免多次访问used
for (int i = 0; i < nums.size(); i++) {
if (!used[i]) { // 只处理未使用的元素
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, res, path, used);
path.pop_back();
used[i] = false;
}
}
}
常见问题及解决方法
避免重复解:
-
排序数组后跳过相同元素
-
使用集合记录已访问路径
提高效率:
-
使用引用传递减少拷贝开销
-
尽量使用基本类型而非容器
处理大型问题:
-
迭代加深搜索
-
双向回溯
练习题
-
组合问题(LeetCode 77)
-
子集问题(LeetCode 78)
-
组合总和(LeetCode 39)
-
分割回文串(LeetCode 131)
-
解数独(LeetCode 37)
总结
回溯算法是解决组合优化问题的强大工具,核心在于:
-
递归 + 回溯的基本框架
-
剪枝优化提高效率
-
合理的数据结构记录状态
掌握回溯算法需要大量练习,建议从简单问题开始,逐步理解回溯的思维模式。记住:回溯的本质是暴力搜索的优化,通过剪枝减少不必要的计算。
学习建议:每解决一个问题后,尝试画出其决策树,理解回溯的过程,这对掌握算法非常有帮助!
附录:回溯算法模板(C++)
void backtrack(参数) {
if (终止条件) {
保存结果;
return;
}
for (选择 : 本层选择集合) {
// 可选:剪枝操作
if (不满足条件) continue;
处理当前选择;
backtrack(新参数); // 递归
撤销选择; // 回溯
}
}