引言
如果给你一道题,给定两个整数 10 和
2,让你返回范围[1, 10]中所有可能的2个数的组合。我们可能会用嵌套两层for循环求解。
那如果给定两个整数100和10呢,1000和100呢,又或者是完全未知的n和k呢?这时候就连for循环嵌套都无从下手。
这时候,就需要回溯算法。
回溯算法是什么
- 回溯算法本质是一种暴力的穷举。
- 回溯法解决的问题都可以抽象为树形结构。
- 并且回溯算法有其固定的模板。
回溯算法五大经典题型和模板
- 回溯算法主要解决五类题型:
|
题型 |
核心目标 |
|---|---|
|
组合问题 |
N 个数中按规则找 k 个数的集合 |
|
切割问题 |
字符串按规则切割的方式数 |
|
子集问题 |
N 个数集合中符合条件的子集数量 / 种类 |
|
排列问题 |
N 个数按规则全排列的方式数 |
|
棋盘问题 |
解决 N 皇后、解数独等棋盘类约束问题 |
- 回溯算法的模板:
void backtracking(参数) {
//1.判断终止
if (终止条件) {
存放结果;
return;
}
//2.遍历集合,收集元素
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
//3.递归和回溯
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
题型一:组合问题
- 此处给出的例题是力扣 77.组合
- 分析:
在套用回溯算法的过程中需要注意去重,因为[1,2]和[2,1]是一个组合,不能重复算入。
本题树形结构

本题模板套用
vector<int> path;
vector<vector<int>> result;
void backtracking(int n, int k, int startidx) {
//1.判断终止(当path内存入数据大于等于k终止)
if (path.size() >= k) {
result.push_back(path);
return;
}
//2.遍历集合,i从startidx开始是去重操作,收集元素
for (int i = startidx; i <= n; i++) {
path.push_back(i);
//3.递归和回溯,pop掉path最后一个元素是溯回的过程
backtracking(n, k, i + 1);
path.pop_back();
}
}
- 完整题解代码:
class Solution { public: void backtracking(int n, int k, int startidx) { if (path.size() >= k) { result.push_back(path); return; } for (int i = startidx; i <= n; i++) { path.push_back(i); backtracking(n, k, i + 1); path.pop_back(); } } vector<vector<int>> combine(int n, int k) { backtracking(n, k, 1); return result; } vector<int> path; vector<vector<int>> result; };
题型二:切割问题
- 此处给出的例题是力扣 131.分割回文串

- 分析:
本题和组合问题非常像,在套用回溯算法的过程中要注意去重,已经截取过的部分得去掉,不能重复截取。
本题树形结构

本题模板套用
void backtracking(string &s, int startidx) {
//1.判断终止
if (startidx >= s.size()) {
result.push_back(path);
return;
}
//2.去重遍历
for (int i = startidx; i < s.size(); i++) {
//此处判断是否为回文字符串,若是则收集元素,不是就跳过
if (isPalindrome(s, startidx, i)) {
string str = s.substr(startidx, i - startidx + 1);
path.push_back(str);
} else {
continue;
}
//3.递归和回溯
backtracking(s, i + 1);
path.pop_back();
}
}
- 完整题解代码:
class Solution { public: bool isPalindrome(string &s, int startidx, int i) { int left = startidx; int right = i; while (left < right) { if (s[left] != s[right]) { return false; } left++; right--; } return true; } void backtracking(string &s, int startidx) { if (startidx >= s.size()) { result.push_back(path); return; } for (int i = startidx; i < s.size(); i++) { if (isPalindrome(s, startidx, i)) { string str = s.substr(startidx, i - startidx + 1); path.push_back(str); } else { continue; } backtracking(s, i + 1); path.pop_back(); } } vector<vector<string>> partition(string &s) { backtracking(s, 0); return result; } vector<string> path; vector<vector<string>> result; };
题型三:子集问题
- 此处给出的例题是力扣 78.子集

- 分析:
本题在大体思路上与组合问题有些相似,但要注意的是,本题在收集结果的过程中,需要收集所有结果,而不是等数组内元素达到一个规定个数再进行收集。
本题树形结构

本题模板套用
vector<int> path;
vector<vector<int>> result;
//引用形式调用是为了提高运行速度
void backtracking(vector<int>& nums, int startidx) {
//先做元素收集是因为要取每一个子集,包括空集
result.push_back(path);
//1.判断终止
if (startidx >= nums.size()) {
return;
}
//2.遍历与元素收集
for (int i = startidx; i < nums.size(); i++) {
path.push_back(nums[i]);
//3.递归与回溯
backtracking(nums, i + 1);
path.pop_back();
}
}
- 完整题解代码:
class Solution { public: vector<int> path; vector<vector<int>> result; void backtracking(vector<int>& nums, int startidx) { result.push_back(path); if (startidx >= nums.size()) { return; } for (int i = startidx; i < nums.size(); i++) { path.push_back(nums[i]); backtracking(nums, i + 1); path.pop_back(); } } vector<vector<int>> subsets(vector<int>& nums) { backtracking(nums, 0); return result; } };
题型四:排列问题
- 此处给出的例题是力扣 46.全排列

- 分析:
本题中[1,2]和[2,1]算作两个可能的全排列,与组合问题不用,也就是说,本题不需要向前去重,但要保证每一个结果内包含nums中的所有元素,所以需要对nums内的每一个元素做记号,来记录其时候已经被录入全排列数组中。(我们这里选择创建一个全为0的used数组,当对应位置的元素被录入结果集合,将0变为1)
本题树形结构

本题模板套用
vector<int> path;
vector<vector<int>> result;
//引用传递减少运行速度
void backtracking(vector<int>& nums, vector<int>& used) {
//1.当path内元素个数与数组个数相同,代表全排列完成
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
//2.遍历,若元素再used数组中未被记录,加入排列中
for (int i = 0; i < nums.size(); i++) {
if (used[i] == 1) {
continue;
}
path.push_back(nums[i]);
//3.递归与溯回,这次不光要怕pop元素,还要清除used数组中对于此元素的记录
used[i] = 1;
backtracking(nums, used);
used[i] = 0;
path.pop_back();
}
}
- 完整题解代码:
class Solution { public: vector<int> path; vector<vector<int>> result; void backtracking(vector<int> &nums, vector<int>& used) { if (path.size() == nums.size()) { result.push_back(path); return; } for (int i = 0; i < nums.size(); i++) { if (used[i] == 1) { continue; } path.push_back(nums[i]); used[i] = 1; backtracking(nums, used); used[i] = 0; path.pop_back(); } } vector<vector<int>> permute(vector<int>& nums) { vector<int> used(nums.size(), 0); backtracking(nums, used); return result; } };
题型五:棋盘问题
- 此处给出的例题是力扣 51. N皇后

- 分析:
我们以每一行(row)为单位,一行内的所有元素遍历,去除不符合条件的(即同行或者同列或者斜角有元素),得出最后的结果。
本题树形结构

本题模板套用
vector<vector<string>> result;
void backtracking(vector<string>& queenChess, int n, int row) {
//1.行数到达最后,就终止
if (row >= n) {
result.push_back(queenChess);
return;
}
//2.遍历每一行的可填空格
for (int i = 0; i < n; i++) {
//此处判断是否为可填入空格
if (isValid(row, i, queenChess, n)) {
queenChess[row][i] = 'Q';
//3.递归与溯回
backtracking(queenChess, n, row + 1);
queenChess[row][i] = '.';
} else {
continue;
}
}
}
- 完整题解代码:
class Solution { public: bool isValid(int row, int column, vector<string>& queenChess, int n) { for (int i = 0; i < row; i++) { if (queenChess[i][column] == 'Q') { return false; } } for (int i = row - 1, j = column - 1; j >= 0 && i >= 0; i--, j--) { if (queenChess[i][j] == 'Q') { return false; } } for (int i = row - 1, j = column + 1; i >= 0 && j < n; i--, j++) { if (queenChess[i][j] == 'Q') { return false; } } return true; } vector<vector<string>> result; void backtracking(vector<string>& queenChess, int n, int row) { if (row >= n) { result.push_back(queenChess); return; } for (int i = 0; i < n; i++) { if (isValid(row, i, queenChess, n)) { queenChess[row][i] = 'Q'; backtracking(queenChess, n, row + 1); queenChess[row][i] = '.'; } else { continue; } } } vector<vector<string>> solveNQueens(int n) { vector<string> queenChess(n, string(n,'.')); backtracking(queenChess, n, 0); return result; } };
总结
回溯算法本质是暴力穷举,问题可抽象为树形结构,有固定解题模板。
核心解决组合、切割、子集、排列、棋盘五类典型问题。
模板核心三步:判断终止、遍历选择元素、递归 + 回溯。
5221

被折叠的 条评论
为什么被折叠?



