递归专题3 - 回溯算法十大类型
递归专题3 - 回溯算法十大类型
本文是递归算法系列的第3篇,完整体系包括:
- 递归基础与思维方法
- 二叉树DFS专题
- 回溯算法十大类型 (本文)
- 网格DFS与回溯
- FloodFill算法专题

📋 目录
- 前言
- 一、回溯的本质
- 1.1 什么是回溯
- 1.2 回溯的关键点
- 二、回溯十大类型总结表
- 三、基础类型详解
- 类型1: 全排列型
- 类型2: 子集/组合型
- 类型3: 多集合组合型
- 类型4: 剪枝生成型
- 类型5: 符号选择型
- 四、高级类型详解
- 类型6: N皇后型
- 类型7: 解数独型
- 五、判断技巧总结
- 六、常见错误
- 七、总结
前言
回溯算法是递归里比较难的一部分。刚开始做回溯题的时候,经常搞不清楚什么时候用check数组,什么时候用index参数,for循环该从0开始还是从index开始。做了二十多道回溯题之后,终于总结出了一套分类方法。
这篇文章把回溯题分成十大类型,每种类型有固定的模板和判断技巧。以后遇到回溯题,先判断是哪一类,然后套用对应的模板,基本就能做出来。
一、回溯的本质
1.1 什么是回溯
回溯就是"尝试-撤销"的过程:
1. 做一个选择
2. 递归处理后续问题
3. 撤销这个选择
4. 尝试下一个选择
回溯的核心框架:
void backtrack(参数) {// 递归出口if (满足条件) {记录结果;return;}// 回溯框架for (遍历选择) {// 做选择修改状态;// 递归backtrack(下一层);// 撤销选择 (恢复现场)恢复状态;}
}
1.2 回溯的关键点
关键1:什么时候恢复现场?
必须在递归之后恢复:
for (...) {path.push_back(x); // 做选择dfs(...); // 递归path.pop_back(); // 撤销 <- 在这里
}
不能在递归出口恢复,因为你不知道该恢复哪个。
关键2:有些状态不需要恢复
如果用的是值传递,参数会自动恢复:
void dfs(vector<int> path) { // 值传递path.push_back(x);dfs(path);// 不需要pop_back,因为path是副本
}
但这样效率低,一般还是用引用+手动恢复。
二、回溯十大类型总结表
做了很多题后,总结出了回溯的十种常见类型:
| 类型 | 集合来源 | check数组 | index参数 | for循环起点 | 递归参数 | 典型题目 |
|---|---|---|---|---|---|---|
| 1. 全排列型 | 同一个数组 | ✅ 需要 | ❌ 不需要 | i=0 | 不变 | LeetCode 46/47 |
| 2. 子集/组合型 | 同一个数组 | ❌ 不需要 | ✅ 需要 | i=index | i+1 | LeetCode 78/77 |
| 3. 多集合组合型 | 不同集合 | ❌ 不需要 | ✅ 需要 | i=0 | index+1 | LeetCode 17 |
| 4. 剪枝生成型 | 同类元素 | ❌ 不需要 | 特殊 | 不需要for | 状态参数 | LeetCode 22 |
| 5. 符号选择型 | 固定元素 | ❌ 不需要 | ✅ 需要 | 不需要for | pos+1 | LeetCode 494 |
| 6. N皇后型 | 二维空间 | ✅ 多个 | ✅ row | i=0 | row+1 | LeetCode 51 |
| 7. 解数独型 | 二维空间 | ✅ 多个 | ✅ pos | i=1-9 | pos+1 | LeetCode 37 |
| 8. 网格DFS(找一条) | 二维网格 | ✅ 二维 | (i,j) | 4方向 | i,j变化 | LeetCode 79 |
| 9. 网格DFS(找最优) | 二维网格 | ✅ 二维 | (i,j) | 4方向 | i,j变化 | LeetCode 1219 |
| 10. FloodFill型 | 二维网格 | ❌ 直接改 | (i,j) | 4方向 | i,j变化 | LeetCode 733/200 |
注:类型8-10属于网格DFS,单独一篇文章讲,这里主要讲前7种。
三、基础类型详解
类型1:全排列型
特征:
- 从同一个数组选元素
- 关心顺序 ([1,2,3] 和 [3,2,1] 算不同)
- 每个元素只能用一次
- 需要check数组标记已用元素
模板:
bool check[n]; // 标记数组
vector<int> path;
vector<vector<int>> ret;void dfs(vector<int>& nums) {// 递归出口:选够了n个if (path.size() == nums.size()) {ret.push_back(path);return;}// 从0开始遍历所有元素for (int i = 0; i < nums.size(); i++) {if (check[i]) continue; // 已用过,跳过// 做选择path.push_back(nums[i]);check[i] = true;// 递归dfs(nums);// 撤销选择path.pop_back();check[i] = false;}
}
关键点:
- for循环从0开始 (因为关心顺序)
- 需要check数组 (防止重复使用)
- 递归参数不需要index (因为每次都从0遍历)
示例:LeetCode 46 全排列
class Solution {
public:vector<vector<int>> ret;vector<int> path;bool check[7];vector<vector<int>> permute(vector<int>& nums) {memset(check, false, sizeof(check));dfs(nums);return ret;}void dfs(vector<int>& nums) {if (path.size() == nums.size()) {ret.push_back(path);return;}for (int i = 0; i < nums.size(); i++) {if (check[i]) continue;path.push_back(nums[i]);check[i] = true;dfs(nums);path.pop_back();check[i] = false;}}
};
类型2:子集/组合型
特征:
- 从同一个数组选元素
- 不关心顺序 ([1,2,3] 和 [3,2,1] 算同一个)
- 可以选任意多个 (子集) 或固定k个 (组合)
与全排列的区别:
| 特性 | 全排列 | 子集/组合 |
|---|---|---|
| 关心顺序 | ✅ | ❌ |
| for起点 | i=0 | i=index |
| 递归参数 | 不变 | i+1 |
| check数组 | 需要 | 不需要 |
模板:
vector<int> path;
vector<vector<int>> ret;void dfs(vector<int>& nums, int index) {ret.push_back(path); // 每个节点都记录 (子集)// 或者 if (path.size() == k) ret.push_back(path); (组合)// 从index开始遍历 (避免重复)for (int i = index; i < nums.size(); i++) {path.push_back(nums[i]);dfs(nums, i + 1); // 传i+1,只往后选path.pop_back();}
}
关键点:
- for从index开始 (不关心顺序,避免重复)
- 递归传
i+1(只选后面的元素) - 不需要check数组 (index自然防止重复)
为什么不需要check数组?
因为index参数保证了只往后选:
index=0: 可选 [0,1,2,3]
index=1: 可选 [1,2,3] <- 0已经不能选了
index=2: 可选 [2,3]
所以不会重复选同一个元素。
示例:LeetCode 78 子集
class Solution {
public:vector<vector<int>> ret;vector<int> path;vector<vector<int>> subsets(vector<int>& nums) {dfs(nums, 0);return ret;}void dfs(vector<int>& nums, int index) {ret.push_back(path); // 每个节点都记录for (int i = index; i < nums.size(); i++) {path.push_back(nums[i]);dfs(nums, i + 1);path.pop_back();}}
};
示例:LeetCode 77 组合
class Solution {
public:vector<vector<int>> ret;vector<int> path;vector<vector<int>> combine(int n, int k) {dfs(n, k, 1);return ret;}void dfs(int n, int k, int index) {// 只有选够k个才记录 (组合和子集的唯一区别)if (path.size() == k) {ret.push_back(path);return;}for (int i = index; i <= n; i++) {path.push_back(i);dfs(n, k, i + 1);path.pop_back();}}
};
子集 vs 组合:
// 子集:记录所有节点
void dfs(nums, index) {ret.push_back(path); // 每个节点for (i = index ...) { ... }
}// 组合:只记录size==k的节点
void dfs(nums, index) {if (path.size() == k) { // 只记录够数的ret.push_back(path);return;}for (i = index ...) { ... }
}
本质:组合 = 受限的子集 (只要size==k的)
类型3:多集合组合型
特征:
- 每一层从不同的集合选元素
- 典型:电话号码的字母组合
与子集/组合的区别:
| 特性 | 子集/组合 | 多集合组合 |
|---|---|---|
| 集合来源 | 同一个数组 | 不同集合 |
| for起点 | i=index | i=0 |
| 递归参数 | i+1 | index+1 |
模板:
vector<string> mapping = {"abc", "def", "ghi", ...};void dfs(string& digits, int index) {if (index == digits.size()) {ret.push_back(path);return;}// 获取当前层的集合string letters = mapping[digits[index] - '0'];// 从0开始遍历当前集合for (int i = 0; i < letters.size(); i++) {path.push_back(letters[i]);dfs(digits, index + 1); // 传index+1,到下一层path.pop_back();}
}
关键点:
- 每层有自己的集合 (mapping[digits[index]])
- for从0开始 (遍历当前集合)
- 递归传
index+1(到下一层)
示例:LeetCode 17 电话号码的字母组合
class Solution {
public:vector<string> ret;string path;string mapping[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};vector<string> letterCombinations(string digits) {if (digits.empty()) return {};dfs(digits, 0);return ret;}void dfs(string& digits, int index) {if (index == digits.size()) {ret.push_back(path);return;}string letters = mapping[digits[index] - '0'];for (int i = 0; i < letters.size(); i++) {path.push_back(letters[i]);dfs(digits, index + 1);path.pop_back();}}
};
类型4:剪枝生成型
特征:
- 不是选择元素,而是生成序列
- 每步有特定的约束条件
- 需要剪枝保证合法性
模板:
void dfs(int 状态参数) {// 递归出口if (满足条件) {记录结果;return;}// 不需要for循环,直接尝试所有可能if (选择1合法) {path += '(';dfs(新状态);path.pop_back();}if (选择2合法) {path += ')';dfs(新状态);path.pop_back();}
}
示例:LeetCode 22 括号生成
class Solution {
public:vector<string> ret;string path;vector<string> generateParenthesis(int n) {dfs(n, 0, 0);return ret;}void dfs(int n, int left, int right) {// 递归出口if (left == n && right == n) {ret.push_back(path);return;}// 选择1:加左括号 (只要left<n就能加)if (left < n) {path += '(';dfs(n, left + 1, right);path.pop_back();}// 选择2:加右括号 (只有right<left才能加)if (right < left) {path += ')';dfs(n, left, right + 1);path.pop_back();}}
};
关键点:
- 不需要for循环
- 每个选择都有明确的约束条件
- 状态参数决定能做什么选择
类型5:符号选择型
特征:
- 数组的每个元素都必须使用
- 只是给每个元素选择符号/属性
- 不需要for循环
与全排列的区别:
| 特性 | 全排列 | 符号选择 |
|---|---|---|
| 元素必须全用 | ✅ | ✅ |
| 关心顺序 | ✅ | ❌ |
| for循环 | 需要 | 不需要 |
| check数组 | 需要 | 不需要 |
| 递归参数 | 不变 | pos+1 |
模板:
void dfs(vector<int>& nums, int pos, int sum) {// 递归出口:处理完所有元素if (pos >= nums.size()) {if (sum == target) ret++;return;}// 选择1:加正号dfs(nums, pos + 1, sum + nums[pos]);// 选择2:加负号dfs(nums, pos + 1, sum - nums[pos]);
}
关键点:
- 不需要for循环 (每个位置固定k个选择)
- 不需要check数组 (所有元素都用)
- 递归传
pos+1(处理下一个元素)
示例:LeetCode 494 目标和
class Solution {
public:int ret = 0;int findTargetSumWays(vector<int>& nums, int target) {dfs(nums, 0, 0, target);return ret;}void dfs(vector<int>& nums, int pos, int sum, int target) {if (pos >= nums.size()) {if (sum == target) ret++;return;}// 加正号dfs(nums, pos + 1, sum + nums[pos], target);// 加负号dfs(nums, pos + 1, sum - nums[pos], target);}
};
四、高级类型详解
类型6:N皇后型 (多约束排列-逐行填充)
特征:
- 二维空间+多个约束条件
- 每行放一个皇后
- 需要多个check数组检查约束
模板:
bool col[n]; // 列标记
bool diag1[2*n]; // 主对角线标记
bool diag2[2*n]; // 副对角线标记void dfs(int row, int n) {// 递归出口:所有行都放完if (row == n) {ret.push_back(board);return;}// 尝试在第row行的每一列放皇后for (int col = 0; col < n; col++) {// 检查约束int d1 = row - col + n;int d2 = row + col;if (col[col] || diag1[d1] || diag2[d2]) continue;// 放置皇后board[row][col] = 'Q';col[col] = diag1[d1] = diag2[d2] = true;// 递归下一行dfs(row + 1, n);// 撤销board[row][col] = '.';col[col] = diag1[d1] = diag2[d2] = false;}
}
对角线编号技巧:
主对角线 (左上到右下):
row - col = 常数
row - col + n (避免负数) 作为编号副对角线 (右上到左下):
row + col = 常数
row + col 直接作为编号
示例:LeetCode 51 N皇后
class Solution {
public:vector<vector<string>> ret;vector<string> board;bool col_check[10];bool diag1_check[20];bool diag2_check[20];vector<vector<string>> solveNQueens(int n) {board.resize(n, string(n, '.'));memset(col_check, false, sizeof(col_check));memset(diag1_check, false, sizeof(diag1_check));memset(diag2_check, false, sizeof(diag2_check));dfs(0, n);return ret;}void dfs(int row, int n) {if (row == n) {ret.push_back(board);return;}for (int col = 0; col < n; col++) {int d1 = row - col + n;int d2 = row + col;if (col_check[col] || diag1_check[d1] || diag2_check[d2]) continue;board[row][col] = 'Q';col_check[col] = diag1_check[d1] = diag2_check[d2] = true;dfs(row + 1, n);board[row][col] = '.';col_check[col] = diag1_check[d1] = diag2_check[d2] = false;}}
};
类型7:解数独型 (多约束排列-逐格填充)
特征:
- 二维空间,逐格填充
- 每个格子有多个约束 (行/列/宫格)
- 需要bool返回值 (找到一个解就停)
与N皇后的区别:
| 特性 | N皇后 | 解数独 |
|---|---|---|
| 填充方式 | 逐行 | 逐格 |
| 参数 | row | pos (或i,j) |
| 返回值 | void | bool |
| 原因 | 找所有解 | 找一个解 |
模板:
bool row[9][10], col[9][10], grid[3][3][10]; // 三个约束bool dfs(int pos) {// 找到第一个空格while (pos < 81 && board[pos/9][pos%9] != '.') pos++;// 所有格子都填完了if (pos == 81) return true;int i = pos / 9, j = pos % 9;int g = i / 3, h = j / 3;// 尝试填1-9for (int num = 1; num <= 9; num++) {// 检查约束if (row[i][num] || col[j][num] || grid[g][h][num]) continue;// 填数字board[i][j] = num + '0';row[i][num] = col[j][num] = grid[g][h][num] = true;// 递归if (dfs(pos + 1)) return true; // 找到解就返回// 撤销board[i][j] = '.';row[i][num] = col[j][num] = grid[g][h][num] = false;}return false;
}
关键点:
- 用bool返回值 (找到一个解就停)
if (dfs(...)) return true;实现剪枝- 三个check数组:行/列/宫格
五、判断技巧总结
面对一道回溯题,怎么判断是哪种类型?
Step 1: 看集合来源
- 同一个数组 -> 全排列/子集/组合
- 不同集合 -> 多集合组合
- 二维空间 -> N皇后/解数独/网格DFS
- 生成序列 -> 剪枝生成Step 2: 关心顺序吗?
- 关心顺序 -> 全排列 (check数组, for从0开始)
- 不关心顺序 -> 子集/组合 (index参数, for从index开始)Step 3: 所有元素都用吗?
- 都用,只选符号 -> 符号选择 (不需要for)
- 选部分 -> 子集/组合Step 4: 有多个约束吗?
- 行列对角线约束 -> N皇后
- 行列宫格约束 -> 解数独
六、常见错误
6.1 恢复现场的时机不对
// 错误:在出口恢复
if (满足条件) {ret.push_back(path);path.pop_back(); // 错!你不知道该恢复哪个
}// 正确:在递归后恢复
for (...) {path.push_back(x);dfs(...);path.pop_back(); // 对!
}
6.2 全排列和子集搞混
// 全排列:for从0开始,需要check
for (int i = 0; i < n; i++) {if (check[i]) continue;...
}// 子集:for从index开始,不需要check
for (int i = index; i < n; i++) {// 不需要check...
}
6.3 忘记check数组初始化
// 错误:忘记初始化
bool check[10];
dfs(...); // check里是随机值!// 正确:初始化
bool check[10];
memset(check, false, sizeof(check));
dfs(...);
6.4 递归参数传错
// 子集/组合:传i+1
dfs(nums, i + 1);// 多集合:传index+1
dfs(digits, index + 1);// 符号选择:传pos+1
dfs(nums, pos + 1, ...);
七、总结
回溯算法的十大类型:
基础类型:
- 全排列:check数组, for从0开始
- 子集/组合:index参数, for从index开始
- 多集合:每层不同集合, for从0开始
- 剪枝生成:不需要for, 有约束条件
- 符号选择:不需要for, 固定k个选择
高级类型:
6. N皇后:逐行填充, 多个check, void返回值
7. 解数独:逐格填充, 三个check, bool返回值
8-10. 网格DFS (见下一篇)
判断技巧:
- 关心顺序 -> 全排列
- 不关心顺序 -> 子集/组合
- 所有元素都用+选符号 -> 符号选择
- 每层不同集合 -> 多集合组合
- 生成序列+约束 -> 剪枝生成
- 二维+逐行 -> N皇后
- 二维+逐格 -> 解数独
掌握这十种类型和判断技巧,大部分回溯题都能快速分类并套用模板。
系列文章
- 递归基础与思维方法
- 二叉树DFS专题
- 回溯算法十大类型 (本文)
- 网格DFS与回溯
- FloodFill算法专题
