代码随想录算法训练营 Day43 | 图论 part01

一、DFS 与 BFS 本质对比

维度 DFS(深度优先搜索) BFS(广度优先搜索)
核心思想 一条路走到黑,不撞南墙不回头 一层一层往外扩,像水波纹扩散
数据结构 栈(递归调用栈 或 手动 stack 队列(queue
访问顺序 先访问深层节点,后回溯访问兄弟节点 先访问同层节点,再访问下一层节点
空间开销 通常更小(栈深 = 当前路径长度) 通常更大(队列要存整层的节点)
典型适用场景 找路径(所有路径/任意一条路径)、连通性判断、拓扑排序 最短路径(无权图)、层序遍历、最少步数

二、核心区别图解

复制代码
图的遍历顺序示例(从节点1出发):

        1
       / \
      2   3
     / \   \
    4   5   6

DFS 访问顺序: 1 → 2 → 4 → 5 → 3 → 6    (一条道走到底,再回头)
BFS 访问顺序: 1 → 2 → 3 → 4 → 5 → 6    (一层一层铺开)

三、代码模板对比

DFS 模板(递归写法)

cpp 复制代码
void dfs(图/节点, visited) {
    // 1. 终止条件(找到目标 / 越界)
    
    // 2. 处理当前节点(记录路径、标记 visited 等)
    visited[x] = true;
    
    // 3. 遍历所有邻接节点
    for (邻居 : 当前节点的所有邻居) {
        if (!visited[邻居]) {
            dfs(邻居);      // 递归深入
        }
    }
    
    // 4. 回溯(撤销状态)
    visited[x] = false;
}

BFS 模板(队列写法)

cpp 复制代码
void bfs(起点) {
    queue<int> q;
    q.push(起点);
    visited[起点] = true;

    while (!q.empty()) {
        int cur = q.front(); q.pop();  // 取出队头
        
        // 处理当前节点
        
        for (邻居 : cur 的所有邻居) {
            if (!visited[邻居]) {
                visited[邻居] = true;  // 入队时就标记,防止重复入队!
                q.push(邻居);          // 邻居入队
            }
        }
    }
}

四、记忆口诀

DFS 是"一根线"------ 用栈,走到底,求路径。

BFS 是"一张网"------ 用队列,逐层扩,求最短。

五、什么时候选 DFS,什么时候选 BFS?

问题类型 推荐算法 原因
求所有从A到B的路径 DFS DFS 天然带回溯,能穷举出所有路径
求无权图中A到B的最短路径 BFS BFS 逐层扩展,第一次到达终点就是最短路径
判断两个节点是否连通 都可以 DFS 更省代码,BFS 也没问题
求最少步数/最少操作次数 BFS 本质就是最短路问题
拓扑排序 DFS(或 BFS/Kahn) DFS 的后序遍历天然适合拓扑排序
岛屿数量 / 连通块数量 都可以 遍历 + visited,哪个顺手用哪个

98. 可达路径

题目描述

给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。

输入描述

第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边

后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径

输出描述

输出所有的可达路径,路径中所有节点之间空格隔开,每条路径独占一行,存在多条路径,路径输出的顺序可任意。如果不存在任何一条路径,则输出 -1。

注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 `1 3 5`,而不是 `1 3 5 `, 5后面没有空格!

输入示例

复制代码
5 5
1 3
3 5
1 2
2 4
4 5

输出示例

复制代码
1 3 5
1 2 4 5
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

vector<int> path;               // 全局变量:记录当前从起点走过的路径
vector<vector<int>> ans;        // 全局变量:记录所有从起点到终点的完整路径集合

// 深度优先搜索函数
// graph: 图的邻接表
// visited: 节点访问状态数组,防止图中有环导致死循环
// x: 当前正在处理的节点编号
// n: 目标终点编号
void dfs(vector<vector<int>>& graph, vector<bool>& visited, int x, int n) {
    // 1. 终止条件:如果当前节点就是终点,说明找到了一条有效路径
    if (x == n) {
        ans.push_back(path);     // 将当前路径存入结果集合中
        return;                  // 结束当前分支的递归,开始回溯
    }
    // 2. 防环处理:如果当前节点在本次路径中已经被访问过,说明遇到了环,直接放弃这条分支
    if (visited[x]) return;
    // 3. 做选择(进层):标记当前节点为已访问
    visited[x] = true;
    // 4. 遍历当前节点 x 的所有邻接节点(出边)
    for (int i : graph[x]) {
        path.push_back(i);       // 将邻接节点加入当前路径
        dfs(graph, visited, i, n); // 递归深入,以该邻接节点为起点继续往下找
        path.pop_back();         // 撤销选择(回溯):从该邻接节点退回来,尝试其他分支
    }
    // 5. 撤销状态(回溯):将当前节点恢复为未访问状态
    // 【极其重要】因为从其他路径再次到达该节点时,它是合法的,不能被永久锁死
    visited[x] = false;
}

int main() {
    // 1. 读取节点数 n 和 边数 m
    int n, m; 
    cin >> n >> m;
    // 2. 初始化邻接表,因为有 n 个节点(编号 1~n),所以开 n+1 的大小,下标 0 不用
    vector<vector<int>> graph(n + 1);
    vector<bool> visited(n + 1, false); // 初始化所有节点均为未访问
    // 3. 读入 m 条边,构建有向图
    for (int i = 0; i < m; i++) {
        int s, t; 
        cin >> s >> t;
        graph[s].push_back(t);   // 节点 s 指向节点 t
    }
    // 4. 准备起点:题目要求从节点 1 出发,先将其压入路径中
    path.push_back(1);
    // 5. 启动深度优先搜索
    dfs(graph, visited, 1, n);
    // 6. 格式化输出所有找到的路径
    for (auto i : ans) {
        // 输出路径中除最后一个节点外的所有节点,后面加空格
        for (int j = 0; j < i.size() - 1; j++) {
            cout << i[j] << " ";
        }
        // 输出最后一个节点,后面直接换行,不加多余空格
        cout << i.back() << endl;
    }
    return 0;
}

总结

1. 核心思想:回溯三步曲
  • 做选择(进层前):visited[x] = true;(锁定节点,防止在下面递归时绕圈子)和 path.push_back(i);(把路走上去)。
  • 递归(探索深处):dfs(..., i, n);(看看这条路走下去能不能到终点)。
  • 撤销选择(退层后):path.pop_back();visited[x] = false;(这条路走完了或者走不通,退回来,把脚印抹去,让别的路可以走过来)。
2. 为什么一定要有 visited[x] = false;

这是图和树最大的区别。树是没有环的,所以树的回溯通常不需要恢复 visited 状态;但图是有环的。

假设有路径 A -> B -> C,你走通了。如果没有 visited[x] = false;,节点 B 就被永久标记为走过了。接下来当你想走 A -> D -> B 时,一查发现 B 走过了,直接跳过,这就导致漏掉了合法路径。所以必须还原状态。

3. 为什么要有 if(visited[x]) return;

这是一种防御性编程。虽然在 main 函数第一次调用 dfs 时,1 肯定没被访问过;而且你在 for 循环里也可以加 if(!visited[i]) 来拦截。

但把 if(visited[x]) return; 放在 dfs 函数最前面,相当于给函数加了一把安全锁,无论从哪里调用这个函数,只要传进来一个已经走过的节点,它都能立刻安全刹车,避免逻辑漏洞。

相关推荐
叶小鸡3 小时前
小鸡玩算法-力扣HOT100-堆
数据结构·算法·leetcode
何陋轩3 小时前
【重磅】悟空来了:国产AI编程助手深度测评,能否吊打Copilot?
人工智能·算法·面试
逸风尊者4 小时前
XGBoost模型工程使用
java·后端·算法
LUVK_4 小时前
第七章查找
数据结构·c++·考研·算法·408
khalil10204 小时前
代码随想录算法训练营Day-31贪心算法 | 56. 合并区间、738. 单调递增的数字、968. 监控二叉树
数据结构·c++·算法·leetcode·贪心算法·二叉树·递归
lihihi5 小时前
P9936 [NFLSPC #6] 等差数列
算法
啊我不会诶5 小时前
2024ICPC西安邀请赛补题
c++·算法
谭欣辰5 小时前
C++ 版Dijkstra 算法详解
c++·算法·图论
yuan199975 小时前
C&CG(列与约束生成)算法,来解决“风光随机性”下的微网鲁棒配置问题
c语言·开发语言·算法