图片截图:





一、DFS 核心思想
从根进入,向下走,走到底,向上走;绕树一圈,最后从根退出。 本质:递归 + 系统栈
- 向下递归时:压栈
- 递归返回回溯时:弹栈
系统栈保存:返回地址、局部变量、函数参数。
二、四个触发时机(⭐最重要,对应 4 处 printf)
void dfs(int u, int fa){
printf("入 %d\n",u); // 【1.入】刚访问到u,第一次碰到这个节点(自顶向下,算子)
for(auto v : e[u]){
if(v==fa) continue;
printf("下 %d\n",u); //【2.下】准备走向子节点v之前,可以多次执行!
dfs(v, u);
printf("回 %d\n",u); //【3.回】从子节点v递归返回之后,可以多次执行!
}
printf("离 %d\n",u); //【4.离】u的所有子节点全部遍历完成,即将离开u(自底向上,算父)
}
✅ 一句话区分
- 入(进入节点) :每个节点只会触发 1 次 刚到达 u,第一次访问。做自顶向下的计算(父传给子)
- 下(下走,准备进子节点) :每个子分支触发一次,可以多次 在循环里,每准备访问一个孩子 v 就执行一次。u 有几个子节点,就执行几次
下。 - 回(回来,子节点回溯完毕) :每个子分支触发一次,可以多次 子树
dfs(v)全部跑完,回到 u 的时候执行。u 有几个子节点,就执行几次回。 - 离(离开节点) :每个节点只会触发 1 次 u 所有子节点全部遍历结束,准备退出 u。做自底向上的计算(子结果传回父)
口诀:1 入,2 下,3 回,4 离
- 入、离:每个节点仅一次
- 下、回:有多少个子节点,就执行多少次(多次触碰同一节点)
计算方向
- 自顶向下:入、下,父亲把信息传给儿子
- 自底向上:回、离,儿子算完结果传回父亲(子算父)
三、树的 DFS vs 图的 DFS
1)树 DFS(无环,用父节点 fa 判断,不用 vis 数组)
void dfs(int u, int fa){
for(auto v:e[u]){
if(v == fa) continue; // 不走回父节点
dfs(v,u);
}
}
fa:记录 u 的父节点,防止往回走。树没有环,不需要 vis 标记。
2)图 DFS(有环!必须 vis 标记)
图存在回边,只靠 fa 不够,要用vis[]记录节点是否已经访问过 ,避免死循环,遍历生成DFS 树
int vis[N];
void dfs(int u){
vis[u]=true; //标记已访问(入的时候标记)
for(auto v:e[u]){
if(vis[v]) continue;
dfs(v);
}
}
DFS 树:对图做深度优先遍历,遍历过程经过的边构成一棵树,原图剩下的边叫回边(红色打叉的边,不会进 DFS 树)
四、递归与系统栈原理
递归本质靠系统栈自动维护状态:
- 向下走(递归调用子节点):压栈,保存当前函数现场
- 走到叶子无法继续:开始回溯
- 向上返回(递归结束):弹栈,恢复现场,执行
回
五、简单例子理解次数(拿第一张图的树)
节点5有 3 个子节点:2,6,7
- 入:节点 5,1 次
- 下:准备去 2、准备去 6、准备去 7 → 3 次下
- 回:从 2 回来、从 6 回来、从 7 回来 → 3 次回
- 离:全部子树走完,离开节点 5,1 次
所以:下和回是在 for 循环内部,循环几次,就跑几次;入和离在循环外面,每个节点一次。
六、什么时候在哪写代码(做题经验)
- 入:记录节点深度、标记访问、初始化节点信息(父→子传值)
- 下:进入子树前预处理,保存当前状态
- 回:子树计算完成,收集子节点结果(子→父汇总,树形 DP 最常用!)
- 离:节点全部子树处理完,计算当前节点最终信息、回溯撤销标记(回溯题)
七、易错点总结
- 树 dfs 参数
fa,图 dfs 用vis数组,不要混用。 - 下、回在 for 循环内,多次执行;入、离在循环外,每个节点仅一次。
- 图遍历会产生 DFS 树,原图中没有被选进 DFS 树的边,是回边。
- 递归深度太大容易栈溢出(系统栈容量有限),大数据量要手写栈实现迭代版 DFS。
题目1 迷宫方案数



题目2 跳马方案数


题目3 八皇后


题目4 水坑计数


