回溯算法小结
一. 回溯算法介绍
1.1 定义
回溯算法是指:用深度优先搜索对可行解进行暴力枚举,并在枚举后撤销当前操作,从而反复递归进行的搜索算法。
1.2 特征
经过总结,回溯算法主要有如下特征:
- 尝试【新的变化】
- 以该【新的变化】作为新的迭代入口进行迭代
- 撤销【新的变化】
- 重复以上过程
二. 应用举例
2.1 Leetcode 46 全排列
代码如下:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;
backtracking(nums, ans, 0);
return ans;
}
void backtracking(vector<int>& nums, vector<vector<int>> ans, int pos) {
// 递归出口(回溯法是递归,也要遵循递归的写法)
if (pos = nums.size() - 1) {
ans.push(nums);
return ;
}
// 递归条件
backtrcking(nums, ans, pos + 1); // 不交换顺序,它本身也是一个解
for (int i = pos + 1; i < nums.size(); ++i) { // 重复以下过程
swap(nums[pos], nums[i]); // 尝试【新的变化】
backtracking(nums, ans, i); // 以该【新的变化】作为新的迭代入口进行迭代
swap(nus[pos], nums[i); // 撤销【新的变化】
}
}
2.2 Leetcode 39 组合总和
代码如下:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans; // 若尝试结果正确,则添加到ans中
vector<int> path; // 保存每次尝试结果
backtracking(candidates, target, path, ans, 0);
return ans;
}
void backtracking(vector<int>& candidates, int target, vector<int>& path, vector<vector<int>>& ans, int pos) {
// 不需要规定退出条件,因为下面的target > candidates[i]提前做了限制
for (int i = pos; i < candidates.size(); ++i) { // 重复以下过程
path.push_back(candidates[i]); // 尝试【新的变化】
if (target == candidates[i]) {
ans.push_back(path); // 以该【新的变化】作为新的迭代入口进入循环
}
else if (target > candidates[i]) {
backtracking(candidates, target - candidates[i], path, ans, i);
}
path.pop_back(); // 撤销【新的变化】
}
}
2.3 Leetcode 40 组和总和2
代码如下:
class Solution {
private:
vector<pair<int, int>> freq;
vector<vector<int>> ans;
vector<int> tmp;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
for (int n: candidates) {
if (freq.empty() || freq.back().first != n) {
freq.push_back(make_pair(n, 1));
} else {
freq.back().second++;
}
}
dfs(0, target);
return ans;
}
void dfs(int idx, int target) {
if (target == 0) {
ans.push_back(tmp);
return;
}
if (idx == freq.size() || freq[idx].first > target) { // idx超过了数组 或 数组当前位置的数比所需要的大
return;
}
// 不选idx对应的数
dfs(idx + 1, target);
// 选idx对应的数,但是它可能有多个
int more = min(target / freq[idx].first, freq[idx].second);
for (int i = 1; i <= more; ++i) {
tmp.push_back(freq[idx].first);
dfs(idx + 1, target - i * freq[idx].first);
// 这里的idx + 1是表示在freq这个数组下的下一个数字,即下一个不同的数字
}
for (int i = 1; i <= more; ++i) {
tmp.pop_back();
}
}
};
2.3 Leetcode 77 组和
代码如下:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> ans; // 若尝试结果正确,则添加到ans中
vector<int> path; // 保存每次尝试结果
backtracking(n, k, 1, path, ans);
return ans;
}
void backtracking(int n, int k, int pos, vector<int>& path, vector<vector<int>>& ans) {
// 递归出口(回溯法是递归,也要遵循递归的写法)
if (k == 0) {
ans.push_back(path);
return;
}
if (n < k) return;
// 调用循环
for (int i = pos; i <= n; ++i) { // 重复以下过程
path.push_back(i); // 尝试【新的变化】
backtracking(n, k - 1, i + 1, path, ans); // 以该【新的变化】作为新的迭代入口进入循环
path.pop_back(); // 撤销【新的变化】
}
}
也可以将ans和path存储为成员变量。
class Solution {
public:
vector<vector<int>> ans;
vector<int> tmp;
vector<vector<int>> combine(int n, int k) {
com(n, k, 1);
return ans;
}
void com(int n, int k, int idx) {
if (k == 0) {
ans.push_back(tmp);
return;
}
for (int i = idx; i <= n; ++i) {
tmp.push_back(i);
com(n, k - 1, i + 1);
tmp.pop_back();
}
}
};
2.3 Leetcode 79 单词搜索
代码如下:
vector<int> direction{-1,0,1,0,-1};
bool exist(vector<vector<char>>& board, string word) {
bool ans = false;
int m = board.size(), n = board[0].size();
vector<vector<bool>> visited(m, vector<bool>(n, false));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == word[0]) {
visited[i][j] = true;
ans = ans || backtracking(board, word, m, n, i, j, 0, visited);
visited[i][j] = false;
if (ans) { // 减少搜索次数
flag = true;
break;
}
}
}
}
return ans;
}
bool backtracking(vector<vector<char>>& board, string word, int m, int n, int r, int c, int pos, vector<vector<bool>>& visited) {
// 递归出口(回溯法是递归,也要遵循递归的写法)
if (pos == word.length() - 1) return true;
int i = 0, j = 0;
bool ans = false;
for (int k = 0; k < 4; ++k) { // 重复以下过程
i = r + direction[k], j = c + direction[k + 1];
if (i >= 0 && i < m && j >= 0 && j < n && board[i][j] == word[pos + 1] && !visited[i][j]) {
visited[i][j] = true; // 尝试【新的变化】
ans = ans || backtracking(board, word, m, n, i, j, pos + 1, visited); // 以该【新的变化】作为新的迭代入口进入循环
visited[i][j] = false; // 撤销【新的变化】
if (ans) {
break; // 减少搜索次数
}
}
}
return ans;
}
2.3 Leetcode 51 N皇后
N皇后的难点在于比起其他题目,需要更多的代码量,用于处理皇后放置在某位置后更新可行域(put_queen函数),代码如下:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> ans;
vector<vector<int>> attack(n, vector<int>(n, 0));
vector<string> queen(n, string(n, '.'));
backtracking(0, n, queen, attack, ans);
return ans;
}
void backtracking(int k, int n, vector<string>& queen,
vector<vector<int>>& attack, vector<vector<string>>& ans) {
// 递归出口(回溯法是递归,也要遵循递归的写法)
if (k == n) {
ans.push_back(queen);
return;
}
// 遍历第k行所有选择
for (int i = 0; i < n; ++i) { // 重复以下过程
if (!attack[k][i]) {
vector<vector<int>> temp = attack;
queen[k][i] = 'Q'; // 尝试【新的变化】
put_queen(k, i, attack, n); // 皇后放置在某位置后更新可行域
backtracking(k + 1, n, queen, attack, ans); // 以该【新的变化】作为新的迭代入口进入循环
attack = temp; // 撤销【新的变化】
queen[k][i] = '.'; // 撤销【新的变化】
}
}
}
// 皇后放置在某位置后更新可行域
void put_queen(int a, int b, vector<vector<int>>& attack, int n) {
vector<int> dx{-1, -1, -1, 0, 0, 1, 1, 1};
vector<int> dy{-1, 0, 1, -1, 1, -1, 0, 1};
int r = 0, c = 0;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < 8; ++j) {
r = a + i * dx[j], c = b + i * dy[j];
if (r >= 0 && r < n && c >= 0 && c < n) {
attack[r][c] = 1;
}
}
}
}
2.4 华为机考 67 24点
24点有且仅有四种代数运算,分别是加减乘除,我们对目标值进行一次运算,以新的目标值作为下一个迭代的起点。
#include<iostream>
#include<vector>
bool dfs(vector<int> nums, vector<bool>& visited, int target) {
if (target == 0) return true;
bool ans = false;
for (int i = 0; i < 4; ++i) {
if (visited[i]) continue;
visited[i] = true;
ans = ans || dfs(nums, visited, target - nums[i]);
ans = ans || dfs(nums, visited, target + nums[i]);
ans = ans || dfs(nums, visited, target * nums[i]);
if (target % nums[i] == 0) ans = ans || dfs(nums, visited, target / nums[i]);
if (ans) return true;
visited[i] = false;
}
return false;
}
int main() {
vector<int> nums(4, 0);
vector<bool> visited(4, false);
for (int i = 0; i < 4; ++i) cin >> nums[i];
if (dfs(nums, visited, 24)) cout << "true";
else cout << "false";
return 0;
}
2.5 Leetcode 78 子集
class Solution {
public:
vector<vector<int>> ans;
vector<int> tmp;
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
backtracking(nums, n, 0);
return ans;
}
void backtracking(vector<int>& nums, int n, int idx) {
if (idx == n) {
ans.push_back(tmp);
return ;
}
backtracking(nums, n, idx + 1); // 不考虑该点
tmp.push_back(nums[idx]); // 考虑该点
backtracking(nums, n, idx + 1);
if (tmp.back() == nums[idx]) tmp.pop_back();
}
};
我自己也准备了第二种解法,如下所示:
class Solution {
public:
vector<vector<int>> ans;
vector<int> tmp;
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
subsets(nums, 0, n);
return ans;
}
void subsets(vector<int>& nums, int idx, int n) {
if (idx == n) {
ans.push_back(tmp);
return;
}
// 不做添加,直接加入ans
ans.push_back(tmp);
// 按顺序遍历加入ans
for (int i = idx; i < n; ++i) {
tmp.push_back(nums[i]);
subsets(nums, i + 1, n);
tmp.pop_back();
}
}
};
2.6 Leetcode 129 求根节点到叶节点数字之和
class Solution {
public:
int ans = 0, tmp = 0;
int sumNumbers(TreeNode* root) {
if (!root) {
return 0;
}
tmp = 10 * tmp + root->val;
if (!root->left && !root->right) {
ans += tmp;
}
if (root->left) sumNumbers(root->left);
if (root->right) sumNumbers(root->right);
tmp /= 10;
return ans;
}
};
也可以写一个backtracking函数做记录。
class Solution {
public:
int ans = 0, tmp = 0;
int sumNumbers(TreeNode* root) {
if (!root) return 0;
bactracking(root);
return ans;
}
void bactracking(TreeNode* root) {
tmp = 10 * tmp + root->val;
// 如果是末叶节点
if (!root->left && !root->right) {
ans += tmp;
}
if (root->left) bactracking(root->left);
if (root->right) bactracking(root->right);
tmp /= 10;
}
};