leecode每日一题
回溯算法是一种通过试错来解决问题的算法,当发现当前路径无法得到正确解时,会回溯到上一步尝试其他可能。它特别适合解决 组合问题、排列问题、子集问题、棋盘类问题 等。以下是详细解析和C++实现:
一、回溯算法核心思想
“选择 → 探索 → 撤销” 的循环过程:
- 路径:已做出的选择
- 选择列表:当前可以做的选择
- 结束条件:到达决策树底层时的终止条件
二、算法框架模板
void backtrack(路径, 选择列表) {
if (满足结束条件) {
记录结果;
return;
}
for (选择 : 选择列表) {
做选择;
backtrack(新路径, 新选择列表);
撤销选择;
}
}
三、经典问题示例
1. 全排列问题(无重复数字)
问题:给定数组 [1,2,3],返回所有可能的排列。
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
vector<bool> used(nums.size(), false);
backtrack(nums, path, used, res);
return res;
}
void backtrack(vector<int>& nums, vector<int>& path, vector<bool>& used, vector<vector<int>>& res) {
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]);
backtrack(nums, path, used, res);
path.pop_back(); // 撤销选择
used[i] = false;
}
}
2. 组合问题(无重复元素,可重复选取)
问题:从 [2,3,5] 中选和为 8 的组合,如 [2,2,2,2]。
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
vector<int> path;
sort(candidates.begin(), candidates.end()); // 排序以方便剪枝
backtrack(candidates, target, 0, path, res);
return res;
}
void backtrack(vector<int>& nums, int remain, int start, vector<int>& path, vector<vector<int>>& res) {
if (remain < 0) return;
if (remain == 0) {
res.push_back(path);
return;
}
for (int i = start; i < nums.size(); ++i) {
if (nums[i] > remain) break; // 剪枝:后续元素更大,无需尝试
path.push_back(nums[i]);
backtrack(nums, remain - nums[i], i, path, res); // 允许重复,仍从i开始
path.pop_back();
}
}
3. N皇后问题
问题:在N×N棋盘放置皇后,使其不能互相攻击。
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> res;
vector<string> board(n, string(n, '.'));
backtrack(board, 0, res);
return res;
}
void backtrack(vector<string>& board, int row, vector<vector<string>>& res) {
if (row == board.size()) {
res.push_back(board);
return;
}
int n = board[row].size();
for (int col = 0; col < n; ++col) {
if (!isValid(board, row, col)) continue;
board[row][col] = 'Q'; // 放置皇后
backtrack(board, row + 1, res);
board[row][col] = '.'; // 撤销
}
}
bool isValid(vector<string>& board, int row, int col) {
// 检查列
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<board.size(); --i, ++j)
if (board[i][j] == 'Q') return false;
return true;
}
四、关键技巧
- 剪枝优化:通过排序提前终止无效分支(如组合问题)
- 去重处理:使用排序 +
i > start && nums[i] == nums[i-1]
跳过重复元素 - 状态保存:使用
used
数组或索引start
避免重复使用元素
五、时间复杂度分析
回溯算法通常时间复杂度较高(指数级),但通过剪枝可显著优化。例如:
- 全排列问题:O(n×n!)
- 组合问题:O(2^n)
六、练习题目
- 全排列II(含重复数字)
- 子集(无重复元素)
- 组合总和II(每个数字只能用一次)
- 解数独
掌握回溯算法需要理解其递归树结构,并熟练应用剪枝技巧来优化效率。建议从模板入手,逐步理解不同问题的变种处理方式。
AC代码
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
// 对数组进行排序,方便后续去重
sort(nums.begin(), nums.end());
vector<int> combination;
vector<vector<int>> combinations;
// 调用回溯函数生成子集
backtrack(0, nums, combination, combinations);
return combinations;
}
void backtrack(int index, const vector<int>& nums, vector<int>& combination, vector<vector<int>>& combinations) {
// 将当前组合加入结果集
combinations.push_back(combination);
for (int i = index; i < (int)nums.size(); ++i) {
// 跳过重复元素
if (i > index && nums[i] == nums[i - 1]) continue;
// 选择当前元素
combination.push_back(nums[i]);
// 递归生成子集
backtrack(i + 1, nums, combination, combinations);
// 回溯,撤销选择
combination.pop_back();
}
}
};