Files
cpp-courseware/2.Course/1.深入浅出程序设计竞赛(基础篇)/3.简单数据结构/4.图论/讲义资料/3. 图论(三) 讲义.md
T

9.2 KiB
Raw Blame History

C++ 算法:图论(三)- 图的遍历 讲义

一、 引言

1.1 问题引入

我们已经熟悉了数组和树的遍历。数组的遍历是一条直线,从头到尾即可。树的遍历虽有岔路,但结构清晰,从父到子,不会回头。

但是,图的遍历要复杂得多。我们面临着两个新的问题:

  1. 连接的顶点多:在树中,一个节点最多连接少数几个子节点。但在图中,一个顶点可能连接着任意数量的其他顶点,我们该如何选择前进的方向?
  2. 存在“环”:在树中,我们绝不会走回祖先节点。但在图中,从A出发,经过B、C,完全可能再回到A,形成一个“环”。如果程序处理不当,就会陷入无限循环。

让我们通过书本中的**【例18-3 查找文献】**案例,来学习如何解决这些问题。

小K正在阅读一篇编号为1的博客文章。这篇文章引用了若干其他文章作为参考文献,而这些参考文献又各自引用了更多的文章,形成了一个庞杂的引用网络。小K求知欲旺盛,他决定:只要读了一篇文章,就必须把它引用的所有没读过的文章都读一遍。

现在,我们拥有这个引用网络的完整数据(n篇文章,m条引用关系)。问题是:我们如何设计一个策略,能帮助小K不重复、不遗漏地看完所有他从文章1出发能接触到的文章?

我们可以对问题进行抽象为图论问题:

  • 每一篇文章,都可以看作一个顶点。
  • 每一条从文章X到文章Y的引用关系,都可以看作一条从X指向Y的有向边。

因此,这个问题被成功地转化为了一个图论问题:从图的某个起始顶点出发,遍历所有可达的顶点。


二、 核心思想:如何做到“不重复、不遗漏”?

2.1 用visited数组避免重复

为了解决图遍历中可能出现的“环”和“重复访问”问题,我们引入一个关键工具:visited布尔数组。

  • 定义:bool visited[N];
  • 作用:用于记录对应的顶点我们是否已经访问过。visited[i] = true 表示顶点 i 我们已经访问过这个顶点了。
  • 规则:
    1. 每当我们首次访问一个顶点时,立刻在“笔记”上记录下来:visited[i] = true。
    2. 在尝试访问任何一个顶点之前,检查**:if (!visited[i])**。我们只访问还没有到过的顶点。

这个visited数组是保证遍历不重复的关键,是DFS和BFS都必须使用的基础工具。

2.2 DFS 与 BFS 遍历图

解决了不重复的问题,我们如何保证不遗漏呢?有两种策略:

  • 策略一:深度优先(DFS)- “一条路走到底”

    • 当你读了文章A,发现它引用了B和C。你选择立刻深入进去,先把B以及B的所有参考文献、B的参考文献的参考文献……全部看完,直到这条链条的尽头。然后才“回溯”过来看文章C。
    • 一条路走到黑,不撞南墙不回头。
  • 策略二:广度优先(BFS)- “逐层拓展”

    • 当你读完文章A,发现它引用了B和C。你不会立刻深入,而是先把同一层级的B和C都看完。然后,再去看B和C共同引用的下一层级的所有文章。
    • 一层一层地看,像水波一样由近及远地扩散。

三、 深度优先遍历 (DFS)

3.1 算法精讲

DFS体现的是一条路走到底的策略。其过程符合递归的定义:要访问顶点u的所有可达点,我们先访问u,然后对u的每一个邻居v,再去递归地执行“访问v的所有可达点”这一相同的任务。它的两个重要动作是深入探索和回溯。

3.2 步骤拆解(以查找文献的样例数据为例子)
image-20260602155335576

DFS的遍历过程为:

  1. 从1出发:访问1,标记visited[1]=true。1的邻居有2, 3, 4。
  2. 深入2:选择邻居2,递归调用DFS(2)。访问2,标记visited[2]=true。2的邻居有5, 6。
  3. 深入5:选择邻居5,递归调用DFS(5)。访问5,标记visited[5]=true。5没有邻居,已到达尽头。
  4. 回溯到2:从5的调用中返回。检查2的下一个邻居6。
  5. 深入6:选择邻居6,递归调用DFS(6)。访问6,标记visited[6]=true。6没有邻居。
  6. 回溯到2:从6返回。2的所有邻居都已探索完毕。
  7. 回溯到1:从2返回。检查1的下一个邻居3。
  8. 深入3 -> 7 -> 8:重复上述过程,一路深入到8,再层层回溯到1。
  9. 探索4:检查1的下一个邻居4,递归调用DFS(4)。访问4,标记visited[4]=true。
  10. 检查4的邻居:尝试访问7和8,但检查发现visited[7]和visited[8]都已为true,故跳过。
  11. 回溯:从4返回1。1的所有邻居探索完毕。遍历结束。
3.3 代码实现
// DFS函数,v是当前访问的顶点, adj: 邻接表, visited: 访问数组
void DFS(int v) {
    visited[v] = true;  
    cout << v << " ";       // 访问当前节点

    // 遍历v的所有邻居
    for (int neighbor : adj[v]) {
        // 如果邻居未被访问过,则递归深入
        if (!visited[neighbor]) {
            DFS(neighbor);
        }
    }
}

// 题目要求中邻居按小编号优先,所以在注意在主函数中对邻接表排序
for (int i = 1; i <= n; i++) {
    sort(adj[i].begin(), adj[i].end());
}

四、 广度优先遍历 (BFS)

4.1 算法思路

BFS是“逐层扩散”的思路。它必须先处理完最早发现的节点,再处理新发现的节点。这种“先进先出”的规则,契合了**队列(Queue)**的数据结构。

算法流程:

  1. 创建一个队列,并将起始顶点放入队列。标记起始顶点为已访问。
  2. 当队列不为空时,循环执行以下操作: a. 从队头取出一个顶点u。 b. 访问u。 c. 遍历u的所有邻居v,如果v未被访问过,则标记v并将其放入队尾。
4.2 步骤拆解(以查找文献的样例数据为例子)
image-20260602155335576
步骤 操作 队列状态 (Queue) 已访问 (Visited) 输出
1 初始状态,1入队 [1] {1}
2 1出队,访问1,其邻居2,3,4入队 [2, 3, 4] {1,2,3,4} 1
3 2出队,访问2,其邻居5,6入队 [3, 4, 5, 6] {1,2,3,4,5,6} 2
4 3出队,访问3,其邻居7入队 [4, 5, 6, 7] {1,2,3,4,5,6,7} 3
5 4出队,访问4,其邻居8入队 [5, 6, 7, 8] {1,2,3,4,5,6,7,8} 4
... ... ... ... ...
10 队列为空,结束 - -

最终访问顺序:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8

4.3 代码实现
// DFS函数,v是当前访问的顶点, adj: 邻接表, visited: 访问数组
void BFS(int v) {
    queue<int> q;

    q.push(v);         // 起始点入队
    visited[v] = true; 

    while (!q.empty()) {
        int u = q.front();  q.pop();   // 取出队头
        
        cout << u << " ";              // 访问

        // 根据邻接表遍历所有邻居
        for (int v : adj[u]) {
            if (!visited[v]) {
                visited[v] = true; 
                q.push(v);         // 新发现的顶点入队
            }
        }
    }
}
// main函数与DFS版本只是调用函数不同而已

五、 总结与应用

5.1 如何将现实问题抽象为图?

图的表达能力极强,世界上绝大多数涉及“物体”和“连接”的问题,都可以抽象为图。只需问自己两个问题:

  1. 物体是什么? -> 它们成为图的顶点。
  2. 连接是什么? -> 它们成为图的边。
  • 社交网络:用户是顶点,好友关系是边。找“朋友的朋友”就是图的遍历。
  • 城市导航:路口是顶点,道路是边。找“最短路线”就是图的最短路径搜索。
5.2 拓展:非连通图的遍历

在本题中,我们只需从顶点1开始遍历。但如果一个图由多个互不相连的部分组成(非连通图),我们该如何遍历所有顶点?

解决办法:在 main 函数中使用一个 for 循环,从 1 到 n 检查每一个顶点 i。如果发现 !visited[i],就说明我们发现了一个新的、独立的连通块,此时再以 i 为起点,调用DFS或BFS来探索这个新部分。

for (int i = 1; i <= n; i++) {
    if (!visited[i]) {   // 如果还没有被访问过,从点i开始遍历这个新的连通图
        DFS(i); // 或 BFS(i)
    }
}