Lecture_Notes:图论算法:DFS与BFS在路径搜索中的应用

Lecture_Notes:图论算法:DFS与BFS在路径搜索中的应用

【免费下载链接】Lecture_Notes This repository is there to store the combined lecture notes of all the lectures. We are using markdown to write the lecture notes. 【免费下载链接】Lecture_Notes 项目地址: https://gitcode.com/GitHub_Trending/lec/Lecture_Notes

图论算法是计算机科学中的重要基础,而深度优先搜索(DFS)和广度优先搜索(BFS)作为两种核心的图遍历算法,在路径搜索、拓扑排序、连通性分析等场景中有着广泛应用。本文将结合Lecture_Notes项目中的图论笔记,详细解析DFS与BFS的原理、实现及在路径搜索中的实际应用,并通过具体案例对比两者的适用场景。

图的基本概念与存储方式

在深入算法之前,我们需要先理解图(Graph)的基本定义和存储结构。图由顶点(Vertex)和边(Edge)组成,根据边的方向和权重可分为有向图、无向图、加权图等类型。在计算机中,图的存储主要有两种方式:

邻接矩阵(Adjacency Matrix)

邻接矩阵是一个二维数组,其中mat[i][j]表示顶点ij之间是否存在边。对于无权图,存在边则值为1,否则为0;对于加权图,值为边的权重。

示例
无向图的邻接矩阵表示如下:

// 伪代码:构建无向图邻接矩阵
int mat[N+1][M+1] = {0}; // 初始化N行M列矩阵
for each edge (u, v):
    mat[u][v] = 1;  // 无向图需同时更新两个方向
    mat[v][u] = 1;

优缺点

  • 优点:判断两点间是否有边的时间复杂度为O(1),适合稠密图。
  • 缺点:空间复杂度为O(V²),对于顶点数较多(如V>10³)的图会造成内存浪费。

邻接表(Adjacency List)

邻接表通过链表数组存储图,每个顶点对应一个链表,记录其所有相邻顶点。这种方式能有效节省空间,尤其适合稀疏图。

示例
无向图的邻接表构建如下:

// 伪代码:构建无向图邻接表
let graph = new Array(N+1).fill().map(() => []); // 初始化N+1个空链表
for each edge (u, v):
    graph[u].push(v);  // 添加双向边
    graph[v].push(u);

空间复杂度:O(V + E),其中V为顶点数,E为边数,是实际应用中更常用的存储方式。详细分析可参考图论基础笔记中的空间复杂度对比。

深度优先搜索(DFS):深入路径的递归遍历

DFS的核心思想

DFS(Depth-First Search)从起始顶点出发,沿着一条路径尽可能深入地搜索,直到无法继续时回溯,探索其他未访问的路径。其本质是利用递归或栈实现的“不撞南墙不回头”的遍历策略。

算法步骤

  1. 标记当前顶点为已访问。
  2. 递归访问当前顶点的所有未访问邻接顶点。
  3. 若所有邻接顶点均已访问,则回溯到上一顶点。

代码实现(递归版)

// 伪代码:DFS递归实现
let visited = new Array(N+1).fill(false); // 记录顶点访问状态
function dfs(u) {
    visited[u] = true; // 标记当前顶点为已访问
    for (let v of graph[u]) { // 遍历所有邻接顶点
        if (!visited[v]) { // 若未访问,则递归访问
            dfs(v);
        }
    }
}

应用场景:环检测与路径搜索

DFS在有向图的环检测中具有天然优势。通过维护一个“当前路径栈”,若遍历过程中遇到已在栈中的顶点,则说明存在环。例如,在课程安排问题中,若检测到环则表示存在循环依赖,无法完成所有课程。

示例
有向图环检测的核心代码如下:

// 伪代码:有向图环检测(DFS)
let visited = new Array(N+1).fill(false);
let path = new Array(N+1).fill(false); // 记录当前递归路径
function hasCycle(u) {
    visited[u] = true;
    path[u] = true;
    for (let v of graph[u]) {
        if (path[v]) return true; // 发现环
        if (!visited[v] && hasCycle(v)) return true;
    }
    path[u] = false; // 回溯时移出路径
    return false;
}

可视化流程
下图展示了DFS在有向图中检测环的过程(图片来源:图论进阶笔记):
DFS环检测示意图

广度优先搜索(BFS):层次遍历的最短路径算法

BFS的核心思想

BFS(Breadth-First Search)从起始顶点出发,优先访问所有距离为1的邻接顶点,再访问距离为2的顶点,以此类推。BFS利用队列(Queue)实现,保证了“层次遍历”的特性,因此天然适用于求解无权图的最短路径问题。

算法步骤

  1. 将起始顶点入队,并标记为已访问。
  2. 队首顶点出队,遍历其所有未访问邻接顶点,依次入队并标记为已访问。
  3. 重复步骤2,直到队列为空。

代码实现(队列版)

// 伪代码:BFS队列实现
function bfs(start) {
    let visited = new Array(N+1).fill(false);
    let q = new Queue(); // 初始化队列
    q.enqueue(start);
    visited[start] = true;
    while (!q.isEmpty()) {
        let u = q.dequeue(); // 出队并处理当前顶点
        for (let v of graph[u]) { // 遍历邻接顶点
            if (!visited[v]) {
                visited[v] = true;
                q.enqueue(v); // 入队下一层顶点
            }
        }
    }
}

应用场景:多源最短路径与矩阵搜索

BFS的层次遍历特性使其在多源最短路径问题中表现突出。例如,“腐烂的橘子”问题中,多个腐烂橘子同时向四周扩散,BFS可计算所有橘子腐烂的最短时间。

示例:腐烂橘子问题的BFS解法

// 伪代码:多源BFS求解腐烂橘子问题
int orangesRotting(int[][] grid) {
    Queue<int[]> q = new LinkedList<>();
    int fresh = 0; // 记录新鲜橘子数量
    // 初始化:将所有腐烂橘子入队
    for (int i = 0; i < grid.length; i++) {
        for (int j = 0; j < grid[0].length; j++) {
            if (grid[i][j] == 2) q.offer(new int[]{i, j, 0});
            else if (grid[i][j] == 1) fresh++;
        }
    }
    // 方向数组:上下左右
    int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
    int time = 0;
    while (!q.isEmpty()) {
        int[] curr = q.poll();
        int x = curr[0], y = curr[1], t = curr[2];
        time = t; // 更新当前时间
        for (int[] d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            if (nx >= 0 && nx < grid.length && ny >=0 && ny < grid[0].length 
                && grid[nx][ny] == 1) {
                grid[nx][ny] = 2; // 标记为腐烂
                fresh--;
                q.offer(new int[]{nx, ny, t+1});
            }
        }
    }
    return fresh == 0 ? time : -1; // 若有剩余新鲜橘子,返回-1
}

案例效果
下图展示了BFS解决“腐烂橘子”问题的扩散过程(图片来源:图论应用笔记):
BFS腐烂橘子扩散示意图

DFS与BFS的对比及适用场景

核心差异

特性DFSBFS
数据结构栈(递归本质是系统栈)队列
遍历顺序深度优先,可能跳过深层节点广度优先,按层次逐层扩散
空间复杂度O(h),h为图的深度(最坏O(V))O(w),w为图的宽度(最坏O(V))
典型应用环检测、拓扑排序、迷宫路径搜索最短路径(无权图)、层序遍历

路径搜索场景对比

  1. 迷宫问题

    • DFS适合探索所有可能路径,但无法保证最短路径。
    • BFS可找到最短路径,但需要存储更多中间状态。
  2. 网格类问题
    在矩阵中搜索连通区域(如“岛屿数量”问题),DFS和BFS均可实现,但BFS更适合求解最短距离。例如,在8方向移动的网格中,BFS能确保找到从起点到终点的最短步数:

    // 伪代码:BFS求解网格最短路径(8方向移动)
    int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
        if (grid[0][0] == 1) return -1;
        int n = grid.size();
        queue<tuple<int, int, int>> q; // (x, y, steps)
        q.emplace(0, 0, 1);
        grid[0][0] = 1; // 标记为已访问
        vector<pair<int, int>> dirs = {{-1,-1}, {-1,0}, {-1,1}, {0,-1}, 
                                       {0,1}, {1,-1}, {1,0}, {1,1}};
        while (!q.empty()) {
            auto [x, y, s] = q.front(); q.pop();
            if (x == n-1 && y == n-1) return s;
            for (auto [dx, dy] : dirs) {
                int nx = x + dx, ny = y + dy;
                if (nx >=0 && nx <n && ny >=0 && ny <n && grid[nx][ny] == 0) {
                    grid[nx][ny] = 1;
                    q.emplace(nx, ny, s+1);
                }
            }
        }
        return -1;
    }
    

总结与扩展

DFS和BFS作为图论的基础算法,在路径搜索中各具优势。DFS通过递归深入探索,适合处理环检测和拓扑排序;BFS借助队列逐层扩散,是求解无权图最短路径的利器。在实际应用中,需根据问题特性选择合适的算法:

  • 若需最短路径或层序遍历,优先使用BFS;
  • 若需探索所有路径或检测环,优先使用DFS。

更多图论算法细节可参考Lecture_Notes项目中的图论进阶笔记BFS应用案例,其中包含完整代码实现和复杂度分析。

【免费下载链接】Lecture_Notes This repository is there to store the combined lecture notes of all the lectures. We are using markdown to write the lecture notes. 【免费下载链接】Lecture_Notes 项目地址: https://gitcode.com/GitHub_Trending/lec/Lecture_Notes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值