GESP 7级
DFS 递归搜索
7级 · DP/图论/搜索
📖 一句话理解
DFS(Depth-First Search,深度优先搜索)就像走迷宫:沿着一条路一直走到底,走不通了就退回来,换一条路继续走。它用递归实现,是搜索类问题的基础。
💡 为什么重要?
DFS是搜索算法的基石。排列、组合、子集、连通性判断、路径搜索……几乎所有需要"枚举所有可能"的问题都可以用DFS解决。掌握DFS的"做选择→递归→撤销选择"框架,你就掌握了搜索类问题的万能模板。
📋 前置知识
- 递归:函数调用自己,知道递归终止条件(base case)
- for循环:基本的循环结构
- 数组/向量:用 vector<int> 或 int[] 存储数据
- swap函数:交换两个变量的值
🧠 DFS的核心框架
📐 DFS万能框架
void dfs(当前状态) {
if (到达终点) {
处理答案; // 比如输出、记录
return;
}
for (每个选择) {
做选择; // 比如标记已使用
dfs(新状态); // 递归探索
撤销选择; // 回溯!
}
}
if (到达终点) {
处理答案; // 比如输出、记录
return;
}
for (每个选择) {
做选择; // 比如标记已使用
dfs(新状态); // 递归探索
撤销选择; // 回溯!
}
}
三个关键要素
🔑 必须搞清楚的三件事
- 状态(参数):当前搜索到了什么位置?比如"已经选了前pos个元素"
- 终止条件:什么时候停下来?比如"所有位置都选完了"
- 选择与回溯:每个位置有哪些选择?选了之后要撤销,才能试下一个选择
🔢 例1:全排列
给定 [1,2,3],输出所有排列。
📊 搜索树(想象一棵树)
从空开始:
├── 选1 → 选2 → 选3 → 得到 [1,2,3]
├── 选1 → 选3 → 选2 → 得到 [1,3,2]
├── 选2 → 选1 → 选3 → 得到 [2,1,3]
├── 选2 → 选3 → 选1 → 得到 [2,3,1]
├── 选3 → 选1 → 选2 → 得到 [3,1,2]
└── 选3 → 选2 → 选1 → 得到 [3,2,1]
共 3! = 6 种排列
💻 全排列代码(逐行注释)
#include <iostream>
#include <vector>
#include <algorithm> // swap()
using namespace std;
int n;
vector<int> path; // 存当前正在构建的排列
vector<bool> used; // used[i] = true 表示数字i已经被选过了
vector<vector<int>> result; // 存所有合法排列
void dfs() {
// 终止条件:path的长度等于n,说明所有位置都选完了
if (path.size() == n) {
result.push_back(path); // 把当前排列加入答案集合
return;
}
// 尝试每个还没被用过的数字
for (int i = 1; i <= n; i++) {
if (!used[i]) { // 如果数字i还没被使用
used[i] = true; // 做选择:标记为已使用
path.push_back(i); // 做选择:加入当前排列
dfs(); // 递归:继续选下一个位置
path.pop_back(); // 撤销选择:从排列中移除
used[i] = false; // 撤销选择:标记为未使用
}
}
}
int main() {
n = 3;
used.assign(n + 1, false); // 初始化used数组(下标1~n)
dfs();
// 输出所有排列
for (auto& p : result) {
for (int x : p) cout << x << " ";
cout << endl;
}
return 0;
}🔢 例2:子集枚举
给定 [1,2,3],输出所有子集(2³=8个)。
💻 子集枚举代码
#include <iostream>
#include <vector>
using namespace std;
int n = 3;
vector<int> path; // 当前子集
vector<vector<int>> result;
void dfs(int start) {
// 每个状态都是一个合法子集(包括空集)
result.push_back(path);
// 从start开始,保证不重复(如 [1,2] 和 [2,1] 视为相同)
for (int i = start; i <= n; i++) {
path.push_back(i); // 做选择:加入i
dfs(i + 1); // 递归:下一个从i+1开始(避免重复)
path.pop_back(); // 撤销选择:移除i
}
}
int main() {
dfs(1);
// 输出所有子集
for (auto& s : result) {
cout << "{ ";
for (int x : s) cout << x << " ";
cout << "}" << endl;
}
return 0;
}🔑 DFS vs BFS 对比
📝 DFS和BFS的区别
- DFS:沿着一条路走到底再回溯,用递归(或栈)实现。适合找所有方案、判断连通性。
- BFS:逐层扩展,用队列实现。适合求最短路径(无权图)。
简单记忆:DFS走迷宫是一条路走到黑,BFS是像水波一样一圈圈扩散。
🚨 易错点
⚠️ 常见错误汇总
- 忘记回溯(撤销选择):这是最最常见的错误!递归返回后,必须把状态恢复原样,否则会影响后续分支。
- 终止条件写错:全排列是 path.size()==n,子集是每一步都记录结果。终止条件决定搜索是否正确。
- 递归参数传递错误:比如子集枚举要用 start 参数避免重复,忘记传递会导致重复方案。
- 没有visited数组:全排列必须记录哪些数字用过,否则同一个数字会被选多次。
- 递归深度太大:DFS是递归实现的,如果搜索深度太大(比如n>1000),可能会栈溢出。
- 剪枝不到位:DFS的效率靠"剪枝"提升——提前排除不可能的分支,否则指数级时间会超时。
🎯 练习建议
📝 循序渐进练习路径
- 先理解框架:把上面的"做选择→递归→撤销选择"框架抄下来,每次写DFS前先套这个框架。
- 手动模拟:在纸上画搜索树,手动模拟递归过程。
- 经典题目:
- P1706 全排列(洛谷入门)
- P7827 小朋友的数字(组合枚举)
- P1443 马的遍历(BFS,但先练DFS理解搜索)
- P1219 八皇后问题(经典DFS剪枝)
- 学会剪枝:在搜索过程中提前判断"这条路不可能有答案"就直接返回,可以大幅提升效率。