在英文技术面试中,若要流畅分析并表达 LeetCode 上回溯算法相关题目的解题思路,以下这些词汇是你必须掌握的:
### 回溯算法基础概念
1. **Backtracking Algorithm(回溯算法)**:一种通过尝试所有可能的解决方案来找到问题的解的算法,当发现当前的选择无法得到有效的解时,会撤销上一步的选择并尝试其他选择。
2. **Solution Space(解空间)**:所有可能的解决方案的集合,回溯算法会在这个空间中进行搜索。
3. **Decision Tree(决策树)**:用于可视化回溯算法搜索过程的树状结构,每个节点代表一个决策点,每条边代表一个选择。
4. **Candidate(候选)**:在每一步中可供选择的元素或选项。
5. **Valid Solution(有效解)**:满足问题所有约束条件的解决方案。
6. **Pruning(剪枝)**:在搜索过程中,提前排除那些不可能产生有效解的分支,以减少不必要的计算。
### 回溯过程相关
1. **Explore(探索)**:尝试当前的选择,深入到决策树的下一层。
- **Explore a Candidate(探索一个候选)**:对当前的候选选项进行尝试。
2. **Backtrack(回溯)**:撤销上一步的选择,回到上一个决策点,尝试其他选择。
- **Backtrack to the Previous State(回溯到上一个状态)**:恢复到之前的状态以尝试新的路径。
3. **State(状态)**:在搜索过程中的当前情况,包括已做出的选择、剩余的候选等。
- **Current State(当前状态)**:表示当前所处的状态。
- **Initial State(初始状态)**:搜索开始时的状态。
4. **Path(路径)**:从初始状态到当前状态所经过的选择序列,代表了部分解决方案。
5. **Constraint(约束条件)**:问题中对解决方案的限制,用于判断一个候选是否可以继续探索。
### 代码实现相关
1. **Recursion(递归)**:回溯算法通常使用递归的方式实现,函数调用自身来探索不同的选择。
- **Recursive Function(递归函数)**:用于实现回溯逻辑的函数。
- **Base Case(基本情况)**:递归终止的条件,通常是找到有效解或确定当前路径无法得到有效解。
2. **Stack(栈)**:递归调用本质上使用了系统栈来保存状态信息,有时也会显式使用栈来模拟回溯过程。
3. **Global Variable(全局变量)**:在回溯算法中,有时会使用全局变量来保存最终结果或共享状态。
4. **Parameter(参数)**:递归函数的输入,通常包括当前状态、路径、剩余候选等信息。
5. **Return Value(返回值)**:递归函数的输出,可能表示是否找到有效解等信息。
6. **Loop(循环)**:用于遍历候选选项,尝试不同的选择。
### 常见应用场景相关
1. **Permutation(排列)**:回溯算法常用于生成给定元素的所有排列。
- **Generate Permutations(生成排列)**:使用回溯算法生成元素的不同排列。
2. **Combination(组合)**:生成给定元素的所有组合。
- **Generate Combinations(生成组合)**:通过回溯算法找出元素的不同组合。
3. **Subset(子集)**:找出给定集合的所有子集。
- **Find Subsets(找出子集)**:利用回溯算法得到集合的全部子集。
4. **N - Queens Problem(N 皇后问题)**:在 N×N 的棋盘上放置 N 个皇后,使得它们互不攻击,是经典的回溯算法应用。
5. **Sudoku Solver(数独求解器)**:使用回溯算法来解决数独谜题。
vector<vector<int>> res;
vector<int> path;
void dfs() {
if (递归终止条件){
res.push_back(path);
return;
}
// 递归方向
for (xxx) {
path.push_back(val);
dfs();
path.pop_back();
}
}
1.涉及枚举
2.不确定 for 循环的次数
总结
枚举各种可能的情况。
0.直接枚举子集
1.约束条件是子集中数字的和 39
2.约束条件是子集的大小 77 46 47
3.约束条件是1 2两者的结合 2161
4.约束条件是集合数 + sum 93 698
5.去重:同层删去相同的递归起点
6.约束条件是 子集中数的大小关系 491
7.前一个情况可能是后一个情况的约束 51
77
第一层可以选 1 2 3 4
第二层可以选 234 134 ...
需要 path 存储选择的路径。需要 index 作为元素下标。
class Solution {
public:
// 储存当前路径
vector<int> path;
vector<vector<int>> res;
vector<vector<int>> combine(int n, int k) {
dfs(1, n, k);
return res;
}
void dfs(int index, int n, int k) {
// 比如 k = 2, n = 4, index = 4
// 不足以构成数组,要提前结束
if (path.size() + (n - index + 1) < k) return;
// 如果路径的长度是 k,那么把这个路径加入到结果数组
if (path.size() == k) {
res.push_back(path);
return;
}
// 否则的话,从 index 开始回溯
for (int i = index; i <= n; ++i) {
path.push_back(i);
dfs(i + 1, n, k);
path.pop_back();
}
}
};
39
target = 7
2 2 3/ 2 3 2
3 4
递归树:
s1 2 3 6 7
s2 2367 2367 ...
s3 2367 ...
这一次选了2,下一次从>=2开始选
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//先排序
sort(candidates.begin(), candidates.end());
// 从index = 0的位置开始选,一开始 sum = 0
dfs(0, 0, candidates, target);
return res;
}
void dfs(int index, int sum, vector<int>& candidates, int target) {
if (sum >= target) {
if (sum == target) {
res.push_back(path);
}
return;
}
// 这次选了 index 下次从 index 开始选
for (int i = index; i <= candidates.size() - 1; ++i) {
if (sum + candidates[i] > target) return;
path.push_back(candidates[i]);
// 更新 sum
dfs(i, sum + candidates[i], candidates, target);
path.pop_back();
}
}
};
40 同层去重
和39的区别是不能重复
# 模板
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
dfs(0, 0, candidates, target);
return res;
}
void dfs(int index, int sum, vector<int>& candidates, int target) {
if (sum >= target) {
if (sum == target) {
res.push_back(path);
}
return;
}
// 循环 + 递归
for () {
}
}
};
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
dfs(0, 0, candidates, target);
return res;
}
void dfs(int index, int sum, vector<int>& candidates, int target) {
if (sum >= target) {
if (sum == target) {
res.push_back(path);
}
return;
}
// 同层去重
// 递归起点都是2,那么后面可以不必递归了
unordered_set<int> occ;
for (int i = index; i <= candidates.size() - 1; ++i) {
if (occ.find(candidates[i]) != occ.end()) {
continue;
}
occ.insert(candidates[i]);
path.push_back(candidates[i]);
dfs(i + 1, sum + candidates[i], candidates, target);
path.pop_back();
}
}
};
216 固定数据集
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum3(int k, int n) {
dfs(1, 0, k, n);
return res;
}
void dfs(int index, int sum, int k, int n) {
if (sum >= n) {
// 选 k 个数 和为 n
if (sum == n && path.size() == k) {
res.push_back(path);
}
}
for (int i = index; i <= 9; ++i) {
if (sum + i > n) {
return;
}
path.push_back(i);
dfs(i + 1, sum + i, k, n);
path.pop_back();
}
}
};
93 复原ip地址
遍历字符串长度
根据不同的长度截取子串
class Solution {
public:
vector<string> res;
vector<string> segMent;
vector<string> restoreIpAddresses(string s) {
segMent.resize(4);
dfs(0, 0, segMent, s);
return res;
}
bool check(string s) {
// 要么是 0 要不是 0 开头的
// 字符串转数字
return (s[0] != '0' || s == "0") && stoi(s) < 256;
}
string toString(vector<string> &segMent) {
string res;
for (int i = 0; i < 3; ++i) {
res += segMent[i];
res += '.';
}
res += segMent[3];
return res;
}
//
void dfs(int index, int segId, vector<string> &segMent, string s){
if (index == s.size() || segId == 4) {
if (index == s.size() && segId == 4) {
res.push_back(toString(segMent));
}
return;
}
for (int i = 1; i <= 3; ++i) {
if (index +i > s.size()) return;
string sub;
// 从 index 开始截取长度为 i
sub = s.substr(index, i);
if (check(sub)) {
segMent[segId] = sub;
dfs(index + i, segId + 1, segMent, s);
}
}
}
};
78 子集
123
path 初始为空
1 2 3
23 3 /
3/ /
某 index 数取完就从 index + 1 开始取
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums) {
dfs(0, nums);
return res;
}
void dfs(int index, vector<int>& nums) {
res.push_back(path);
for (int i = index; i <= nums.size() - 1; ++i) {
path.push_back(nums[i]);
dfs(i + 1, nums);
path.pop_back();
}
}
};
491 非递增子序列
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> findSubsequences(vector<int>& nums) {
dfs(0, nums);
return res;
}
unordered_set<int> appear;
void dfs(int index, vector<int>& nums) {
if (path.size() >= 2) {
res.push_back(path);
}
// 同层去重 [4, 6, 6, 7]
unordered_set<int> occ;
for (int i = index; i <= nums.size() - 1; ++i) {
// 确保当前的递归起点没被遍历过
if (occ.find(nums[i]) != occ.end()) continue;
occ.insert(nums[i]);
// 确保序列一直保持递增
if (path.size() > 0 && nums[i] < path.back()) continue;
path.push_back(nums[i]);
dfs(i + 1, nums);
path.pop_back();
}
}
};
46 全排列
每一次都是从头到尾遍历
class Solution {
public:
vector<int> path;
vector<int> used; // 将 vector<bool> 改为 vector<int>
vector<vector<int>> res;
vector<vector<int>> permute(vector<int>& nums) {
used.resize(nums.size(), 0); // 初始化 used 向量
dfs(nums);
return res;
}
void dfs(vector<int>& nums) {
if (path.size() == nums.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i) {
// 如果数字没被用过则改为被用过,且更新path
if (!used[i]) {
used[i] = 1; // 将 false 改为 1,表示已使用
path.push_back(nums[i]);
dfs(nums);
path.pop_back();
used[i] = 0; // 将 true 改为 0,表示未使用
}
}
}
};
47 不重复全排列
和 46 不同的一点是,
[1,1,2] 会出现 [2,1,1] 两次,那么加一个hash去重
class Solution {
public:
vector<vector<int>> res;
vector<int> used;
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
used.resize(nums.size(), 0);
dfs(nums);
return res;
}
void dfs(vector<int>& nums) {
if (path.size() == nums.size()) {
res.push_back(path);
return;
}
// 同层去重,去除递归起点相同的同层元素
unordered_set<int> occ;
for (int i = 0; i < nums.size() ; ++i) {
if (!used[i] && (occ.find(nums[i]) == occ.end())) {
used[i] = 1; // 将 false 改为 1,表示已使用
path.push_back(nums[i]);
dfs(nums);
path.pop_back();
used[i] = 0; // 将 true 改为 0,表示未使用
}
}
}
};
698 k 个等和子集
class Solution {
public:
vector<int> subs;
int ave;
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = 0;
// 桶
subs.resize(k,0);
// 求和
sum = accumulate(nums.begin(), nums.end(), 0);
// 是否能被 k 整除
if (sum % k != 0) {
return false;
}
ave = sum / k;
// 先装大的
//sort(nums.begin(), nums.end(), greater());
sort(nums.begin(), nums.end(), greater());
// 从 0 号位置开始
return dfs(0, nums, k);
}
bool dfs(int index, vector<int>& nums, int k) {
// 如果已经遍历完了所有数字
// 查看每个子集大小
if (index == nums.size()) {
for (auto sub : subs) {
if (sub != ave) {
return false;
}
}
return true;
}
// 对于同一个元素不要尝试和相同的子集
unordered_set<int> occ;
// 核心逻辑
// 分 k 个桶,依次遍历,更新每个桶中元素的值,再 dfs
// dfs 时依次选取不同的数字
// 如果当前的桶再装入一个数字超过 ave
// 去重
for (int i = 0; i < k; ++i) {
if (subs[i] + nums[index] > ave || occ.find(subs[i]) != occ.end()) continue;
occ.insert(subs[i]);
subs[i] += nums[index];
if (dfs(index + 1, nums, k)) return true;
subs[i] -= nums[index];
}
return false;
}
};
51 皇后
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<bool> col(n, false);
vector<bool> diag1(20, false);
vector<bool> diag2(20, false);
vector<int> queens(n, 0);
dfs(0, col, diag1, diag2, queens, n);
return res; // 返回结果集
}
private:
vector<vector<string>> res; // 存储结果集
// 深度优先搜索函数
void dfs(int index, vector<bool>& col, vector<bool>& diag1, vector<bool>& diag2, vector<int>& queens, int n) {
if (index == n) {
res.push_back(generate(queens, n));
return;
}
for (int i = 0; i < n; ++i) {
if (col[i] || diag1[index + i] || diag2[index - i + 9]) continue;
queens[index] = i;// 当前行第 i 列放置皇后
col[i] = diag1[index + i] = diag2[index - i + 9] = true;
dfs(index + 1, col, diag1, diag2, queens, n);
col[i] = diag1[index + i] = diag2[index - i + 9] = false;
}
}
// 生成棋盘
vector<string> generate(vector<int>& queens, int n) {
vector<string> board;
for (int i = 0; i < n; ++i) {
string row(n, '.');
row[queens[i]] = 'Q';
board.push_back(row);
}
return board; // 返回生成的棋盘
}
};
汇编
链接
静态