当前位置: 首页 > news >正文

递归专题3 - 回溯算法十大类型

递归专题3 - 回溯算法十大类型

本文是递归算法系列的第3篇,完整体系包括:

  1. 递归基础与思维方法
  2. 二叉树DFS专题
  3. 回溯算法十大类型 (本文)
  4. 网格DFS与回溯
  5. 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=indexi+1LeetCode 78/77
3. 多集合组合型不同集合❌ 不需要✅ 需要i=0index+1LeetCode 17
4. 剪枝生成型同类元素❌ 不需要特殊不需要for状态参数LeetCode 22
5. 符号选择型固定元素❌ 不需要✅ 需要不需要forpos+1LeetCode 494
6. N皇后型二维空间✅ 多个✅ rowi=0row+1LeetCode 51
7. 解数独型二维空间✅ 多个✅ posi=1-9pos+1LeetCode 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=0i=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=indexi=0
递归参数i+1index+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皇后解数独
填充方式逐行逐格
参数rowpos (或i,j)
返回值voidbool
原因找所有解找一个解

模板:

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, ...);

七、总结

回溯算法的十大类型:

基础类型:

  1. 全排列:check数组, for从0开始
  2. 子集/组合:index参数, for从index开始
  3. 多集合:每层不同集合, for从0开始
  4. 剪枝生成:不需要for, 有约束条件
  5. 符号选择:不需要for, 固定k个选择

高级类型:
6. N皇后:逐行填充, 多个check, void返回值
7. 解数独:逐格填充, 三个check, bool返回值
8-10. 网格DFS (见下一篇)

判断技巧:

  • 关心顺序 -> 全排列
  • 不关心顺序 -> 子集/组合
  • 所有元素都用+选符号 -> 符号选择
  • 每层不同集合 -> 多集合组合
  • 生成序列+约束 -> 剪枝生成
  • 二维+逐行 -> N皇后
  • 二维+逐格 -> 解数独

掌握这十种类型和判断技巧,大部分回溯题都能快速分类并套用模板。


系列文章

  1. 递归基础与思维方法
  2. 二叉树DFS专题
  3. 回溯算法十大类型 (本文)
  4. 网格DFS与回溯
  5. FloodFill算法专题
http://www.dtcms.com/a/554569.html

相关文章:

  • python全栈-数据分析软件tableau的使用
  • 交流电里的电子咋流动?不是往前跑,而是来回 “晃”
  • 做网站写代码怎么样免费网站建设基础步骤
  • 网站.cc域名网站常见结构有那些
  • 网上做兼职老师的正规网站搭建网站的步骤有哪些
  • python进阶教程10:面向对象、super()和元类
  • 大同建设银行保安招聘网站商品展示的网站源码
  • 中交建设集团 网站win10系统可以做网站搭建
  • 做网站建设怎么介绍自己网页图片文字识别
  • 内部类和Object类
  • B049基于博途西门子1200PLC红绿灯控制系统仿真
  • 淘宝手机网站模板下载安装公司网站模板大全
  • 专属虚拟环境:Hugging Face数据集批量下载(无登录+国内加速)完整指南
  • 域名访问网站应该怎么做高端网站建设济南兴田德润简介电话
  • **新一代券商与机构专业交易系统开发:从国际金融变局到技术架构重构**
  • 最好网站建设公司哪家好阳泉集团网站建设
  • 电子商务网站怎么做素材包wordpress 浮窗
  • 海东企业网站建设公司南村网站建设
  • 宁波市高等级公路建设指挥部网站扁平化设计网站
  • e建网站网站设置访问权限
  • 查找(无序线性、有序线性、二分查找)
  • 不同规模企业如何选择与进化营销费用管理?
  • 备案期间网站中小企业
  • .gitignore配置了忽略dist文件夹,但是souretree还是跟踪了dist文件夹的变化怎么解决
  • 网站开发总出现出现404做网站有哪些技术
  • 突破协议壁垒:CCLINK转EtherNet/IP在大型温室罗克韦尔PLC伺服通讯中的实践
  • edu网站一般谁做的网站的seo怎么做
  • 手机触屏网站微信低代码开发平台
  • DAP-Seg:精准解码基因调控赋能科研发现---一种替代ChIP-seq的高效解决方案,让非模式植物研究不再受限
  • 台州临海市建设局网站天琥设计