CSP-J 初赛(以满分为目标):第二十七课 《图的遍历——BFS广度优先搜索——像“水波纹”一样,一层一层地搜索》


第二十七课:图的遍历------BFS广度优先搜索

------像"水波纹"一样,一层一层地搜索

上一课我们学习了:

DFS:深度优先搜索

我们把它想象成走迷宫:

一条路走到底,走不通了再回来。

今天学习一个和 DFS 完全不同、但同样重要的算法:

BFS:广度优先搜索

Breadth First Search


如果说 DFS 是:

"我先钻进去看看!"

那么 BFS 就是:

"别急,我们先把这一圈的人都认识,再认识下一圈。"

这也是图的两种基本遍历方法之一。

我们对 BFS 的定义非常明确:从起始点出发,先访问它的邻接点,再依次访问这些邻接点中尚未访问的邻接点,直到所有顶点都被访问。其基本思想是仿树的层次遍历过程


一、先回忆:DFS是怎么走的?

看这张图:

复制代码
          0
        /   \
       1     2
      / \
     3   4

如果从0开始,按照编号从小到大访问:

DFS会:

复制代码
0
 ↓
1
 ↓
3
 ↑
1
 ↓
4
 ↑
1
 ↑
0
 ↓
2

访问顺序:

0,1,3,4,2

它的特点是:

先深入


二、BFS不一样

如果还是这张图:

复制代码
          0
        /   \
       1     2
      / \
     3   4

BFS从0开始:

第一层:

复制代码
0

第二层:

复制代码
1  2

第三层:

复制代码
3  4

所以:

0,1,2,3,4

这就是:

一层一层地访问


三、BFS到底是什么意思?

把一个点看成水面上的一块石头。

石头扔进水里:

复制代码
        ·
      ~~~~~
    ~~~~~~~~
  ~~~~~~~~~~~

水波纹:

复制代码
第0层:石头自己
第1层:第一圈
第2层:第二圈
第3层:第三圈
......

BFS就是这样:

先访问距离起点近的点,再访问距离起点远的点。

所以:

BFS = 一层一层向外扩散

这也是理解 BFS 最好的方法。


四、为什么BFS像"树的层序遍历"?

我们之前学过二叉树:

复制代码
             1
           /   \
          2     3
         / \   / \
        4   5 6   7

层序遍历:

复制代码
第一层:1
第二层:2 3
第三层:4 5 6 7

结果:

1,2,3,4,5,6,7

这就是:

从上到下,一层一层访问。

BFS的思想完全类似。

所以讲义把 BFS 概括为:

仿树的层次遍历过程。


五、但是图不是树

这里一定要注意。

树:

复制代码
        A
       / \
      B   C
     / \
    D   E

通常不会出现环。

图却可能:

复制代码
A ------ B
|    |
D ------ C

如果BFS不做任何处理:

复制代码
A → B
B → A
A → B
B → A
...

又死循环了。

所以:

BFS同样需要visited数组!


六、DFS和BFS的共同点

这一点非常重要。

虽然:

复制代码
DFS

和:

复制代码
BFS

走法不同,但是它们有共同的基本框架:

复制代码
图
 ↓
从一个点出发
 ↓
寻找邻居
 ↓
只访问没访问过的点
 ↓
visited标记

所以:

复制代码
bool visited[N];

DFS需要。

BFS也需要。


七、DFS和BFS最大的区别是什么?

先看一句非常重要的话:

DFS:先深后广

BFS:先广后深

例如:

复制代码
          A
        /   \
       B     C
      / \     \
     D   E     F

DFS

可能是:

复制代码
A → B → D
        ↓
       回退
        ↓
      B → E
        ↓
       回退
        ↓
      A → C → F

BFS

则是:

复制代码
第一层:
A

第二层:
B C

第三层:
D E F

所以:

复制代码
DFS:钻进去
BFS:铺开来

这是今天第一个必须记住的知识点。


八、BFS最重要的工具:队列

这一课最核心的知识不是代码,而是:

为什么BFS必须使用队列?

答案:

因为BFS要求"先发现的点先处理"。

而队列的特点是什么?

上一阶段我们学习过:

FIFO------先进先出

比如排队买票:

复制代码
小明 → 小红 → 小刚 → 小丽

谁先来?

复制代码
小明

谁先走?

复制代码
小明

所以:

先进入队列的点,先被处理。

这恰好就是BFS需要的。

讲义的栈、队列部分明确说明队列是 FIFO(先进先出),而讲义的相关选择题也直接考过"广度优先搜索时,需要用到的数据结构是队列"。

所以:

BFS → 队列

一定要记住!


九、一个非常好玩的故事

假设学校组织春游。

老师说:

"从小明开始,把所有同学都认识一遍。"

小明认识:

复制代码
小红
小刚

于是:

复制代码
小明
 ↓
小红、小刚

接下来我们先处理:

小红

小红又认识:

复制代码
小丽
小强

于是把:

复制代码
小丽
小强

排到队伍后面。

此时队伍:

复制代码
小刚 → 小丽 → 小强

然后处理:

小刚

小刚认识:

复制代码
小美

加入队伍:

复制代码
小丽 → 小强 → 小美

然后:

复制代码
小丽
小强
小美

一个一个处理。

这就是:

BFS


十、BFS的核心过程

可以浓缩成:

复制代码
起点入队
   ↓
队列不为空?
   ↓
取出队首
   ↓
访问它
   ↓
寻找所有邻居
   ↓
没访问过的邻居入队
   ↓
继续

所以:

BFS = 队列 + visited


十一、BFS第一步:起点入队

假设从:

复制代码
0

开始。

首先:

复制代码
queue<int> q;
q.push(0);

此时:

复制代码
队列:

┌───┐
│ 0 │
└───┘

十二、第二步:标记0已经访问

这里有一个非常重要的细节。

我们可以在:

入队的时候

就标记:

复制代码
visited[0] = true;

为什么?

因为:

0已经决定要进入搜索队伍了。

这样可以防止其他点又把0重复加入队列。

所以:

复制代码
visited[0] = true;
q.push(0);

十三、第三步:取队首

复制代码
int v = q.front();
q.pop();

例如:

复制代码
队列:

0 → 1 → 2

执行:

复制代码
v = q.front();

得到:

复制代码
v = 0

然后:

复制代码
q.pop();

队列变成:

复制代码
1 → 2

这就是:

先进先出


十四、第四步:寻找v的所有邻居

如果:

复制代码
vector<int> g[N];

那么:

复制代码
for (int u : g[v])

就是:

把v的所有邻居找出来。

如果:

复制代码
v = 0

而:

复制代码
g[0] = {1,2}

那么:

复制代码
1
2

就是0的邻居。


十五、第五步:没有访问过就入队

判断:

复制代码
if (!visited[u])

如果u没有访问过:

复制代码
visited[u] = true;
q.push(u);

注意顺序:

复制代码
先标记
再入队

例如:

复制代码
0的邻居:
1、2

于是:

复制代码
队列:
1 → 2

十六、完整BFS模板

这是今天最重要的代码:

复制代码
void bfs(int start)
{
    queue<int> q;

    visited[start] = true;
    q.push(start);

    while (!q.empty())
    {
        int v = q.front();
        q.pop();

        cout << v << " ";

        for (int u : graph[v])
        {
            if (!visited[u])
            {
                visited[u] = true;
                q.push(u);
            }
        }
    }
}

建议初学者不要背,而是要理解:

复制代码
创建队列
↓
起点入队
↓
起点标记
↓
队列不空
↓
取队首
↓
处理它
↓
所有没访问过的邻居入队
↓
继续

十七、逐行解析

复制代码
queue<int> q;

意思:

创建一个队列。


复制代码
visited[start] = true;

意思:

起点已经访问。


复制代码
q.push(start);

意思:

起点进入等待处理的队伍。


复制代码
while (!q.empty())

意思:

只要还有城市等待处理,就继续。


复制代码
int v = q.front();

意思:

看队伍最前面的城市。


复制代码
q.pop();

意思:

这个城市从等待队伍中离开。


复制代码
for (int u : graph[v])

意思:

查看v所有邻居。


复制代码
if (!visited[u])

意思:

这个邻居以前没来过吗?


复制代码
visited[u] = true;
q.push(u);

意思:

标记它,然后让它排队等待。


十八、完整模拟一次BFS

还是这张图:

复制代码
          0
        /   \
       1     2
      / \
     3   4

从0开始。


第一步

复制代码
visited[0] = true

队列:

复制代码
0

第二步

取出0:

复制代码
0

访问0。

发现:

复制代码
1、2

于是:

复制代码
队列:

1 → 2

第三步

取出1:

复制代码
1

访问1。

发现:

复制代码
3、4

加入队列:

复制代码
2 → 3 → 4

第四步

取出2:

复制代码
2

2没有新的点。

队列:

复制代码
3 → 4

第五步

取出3:

复制代码
3

没有新点。


第六步

取出4:

复制代码
4

没有新点。

队列空了。

结束。

最终:

0,1,2,3,4


十九、为什么一定是一层一层?

这其实就是队列造成的。

0发现:

复制代码
1、2

它们进入队列:

复制代码
1 → 2

1又发现:

复制代码
3、4

但是3、4只能排在:

复制代码
2

后面:

复制代码
2 → 3 → 4

所以必须:

先处理2,再处理3、4。

这就是:

第一层没有处理完,第二层不能插队。

而这正是 BFS 的"广度优先"。


二十、BFS和DFS为什么会使用不同的数据结构?

这是非常值得让大家理解的一点。

DFS

想:

"我要一直往里面走。"

所以需要:

复制代码
后进去的先回来

这是:

递归本身就利用了函数调用栈。


BFS

想:

"我先把这一层的人处理完。"

所以需要:

复制代码
先来的先处理

这是:

队列

因此:

复制代码
DFS → 栈
BFS → 队列

可以记成:

深度用栈,广度用队


二十一、一个特别容易考的对应关系

算法 思想 典型数据结构
DFS 深入到底再回退
BFS 一层一层向外扩展 队列

考试如果看到:

"广度优先搜索需要什么数据结构?"

答案:

队列

这一考点,经常考到。


二十二、BFS的一个巨大优势:找最短路

现在出现一个非常重要的问题。

假设:

复制代码
A ------ B ------ D
|         /
C ------ E ------

我们想问:

A到D最少经过几条边?

如果使用BFS:

复制代码
第0层:
A

第1层:
B、C

第2层:
D、E

D第一次被发现的时候:

复制代码
距离 = 2

所以:

A→B→D

是长度2的一条最短路径。

为什么第一次到达D就是最短?

因为BFS是:

按照距离起点从近到远,一层一层搜索。

这是BFS最重要的应用之一。


二十三、用"地铁换乘"理解最短路

假设:

复制代码
A

是你家附近地铁站。

你想去:

复制代码
F

每坐一站算1步。

BFS会:

复制代码
0站:
A

1站:
A能直接到达的站

2站:
再坐一站能够到达的站

3站:
再坐一站能够到达的站

第一次找到F时:

已经保证使用的边数最少。

所以:

无权图最短路 → BFS

这是后面图论非常重要的一条知识。


二十四、"无权图"非常重要

如果边上有不同费用:

复制代码
A --100-- B
A --2---- C
C --2---- B

那么:

复制代码
A → B

虽然只有1条边,但费用是100。

而:

复制代码
A → C → B

有2条边,但费用只有4。

所以:

"边数最少"不等于"权值总和最小"。

因此普通BFS最适合解决:

每条边代价相同的最短路

例如:

复制代码
迷宫
网格移动
每一步代价都相同

以后学习:

复制代码
Dijkstra

再解决:

边权不同的最短路。


二十五、BFS在迷宫中的应用

迷宫:

复制代码
S . . #
# . . #
# . . T

假设:

复制代码
S = 起点
T = 终点

从S开始:

复制代码
第0层:S

第1层:S能走到的位置

第2层:第二步能到的位置

第3层:第三步能到的位置

像水一样:

复制代码
S
↓↓↓
第一圈
↓↓↓
第二圈
↓↓↓
第三圈

第一次到T:

就找到了最少步数。


二十六、这和我们之前学的Flood Fill联系起来了

之前我们学习过:

Flood Fill(泛洪算法)

比如:

复制代码
□□□□□□
□■■□□□
□■■■□□
□□■□□□
□□□□□□

从某一个格子开始:

找上下左右相邻的同类格子。

不断扩散。

这个过程可以使用:

复制代码
DFS

也可以使用:

复制代码
BFS

所以:

Flood Fill = DFS/BFS在网格上的应用

今天学完BFS后,孩子应该能看到一个更大的知识网络:

复制代码
图
│
├── DFS
│    └── 递归 / 栈
│
└── BFS
     └── 队列

而:

复制代码
网格
 ↓
每个格子看成一个顶点
 ↓
相邻格子之间看成边
 ↓
DFS / BFS
 ↓
Flood Fill / 迷宫

二十七、BFS也需要邻接矩阵或邻接表

上一课我们学习:

复制代码
邻接矩阵

和:

复制代码
邻接表

它们只是:

保存图的方法。

DFS、BFS则是:

访问图的方法。

所以:

复制代码
邻接矩阵 / 邻接表
        ↓
      保存图
        ↓
   ┌────┴────┐
   ↓         ↓
  DFS       BFS
   ↓         ↓
  栈        队列

这是非常重要的概念区分。


二十八、邻接表版BFS

我们以后做算法题,非常常见的是:

复制代码
vector<int> graph[N];

BFS:

复制代码
void bfs(int start)
{
    queue<int> q;

    visited[start] = true;
    q.push(start);

    while (!q.empty())
    {
        int v = q.front();
        q.pop();

        cout << v << " ";

        for (int u : graph[v])
        {
            if (!visited[u])
            {
                visited[u] = true;
                q.push(u);
            }
        }
    }
}

这里:

复制代码
queue<int> q;

是BFS的核心。


二十九、邻接矩阵版BFS

如果使用:

复制代码
int graph[N][N];

那么:

复制代码
void bfs(int start)
{
    queue<int> q;

    visited[start] = true;
    q.push(start);

    while (!q.empty())
    {
        int v = q.front();
        q.pop();

        cout << v << " ";

        for (int u = 0; u < n; u++)
        {
            if (graph[v][u] == 1 && !visited[u])
            {
                visited[u] = true;
                q.push(u);
            }
        }
    }
}

区别仅仅是:

"如何找到邻居"不同。

BFS的核心思想完全没有变化。


三十、一个非常重要的细节:什么时候标记visited?

推荐写法:

复制代码
if (!visited[u])
{
    visited[u] = true;
    q.push(u);
}

也就是:

入队时标记

为什么?


看这个图:

复制代码
    A
   / \
  B   C
   \ /
    D

B和C都能发现D。

如果:

复制代码
B → D

D进入队列。

此时应该立刻:

复制代码
visited[D] = true;

这样C再看到D:

复制代码
if (!visited[D])

条件已经不成立。

D不会重复入队。


三十一、如果出队时才标记会怎样?

例如错误思路:

复制代码
q.push(u);

但不立刻:

复制代码
visited[u] = true;

那么:

复制代码
B发现D
C也发现D

可能出现:

复制代码
队列:

D
D

D被重复加入。

所以标准写法是:

发现新点 → 立即标记 → 入队


三十二、BFS遍历一个不连通的图

和DFS一样,如果图不是连通的:

复制代码
A ------ B ------ C

D ------ E

F

从A开始:

复制代码
bfs(A);

只能访问:

复制代码
A B C

不会自动访问:

复制代码
D E F

如果题目要求遍历整个图,也可以:

复制代码
for (int i = 0; i < n; i++)
{
    if (!visited[i])
    {
        bfs(i);
    }
}

于是:

复制代码
第一次BFS → 第一块
第二次BFS → 第二块
第三次BFS → 第三块

这和上一课的DFS完全对应。


三十三、BFS可以统计连通块

例如:

复制代码
A ------ B ------ C

D ------ E

F ------ G

执行:

复制代码
int count = 0;

for (int i = 0; i < n; i++)
{
    if (!visited[i])
    {
        bfs(i);
        count++;
    }
}

那么:

复制代码
第一次:
A B C

第二次:
D E

第三次:
F G

所以:

count=3

这就是:

连通分量数量


三十四、DFS和BFS的终极对比

DFS BFS
中文 深度优先搜索 广度优先搜索
英文 Depth First Search Breadth First Search
思想 一路深入 一层一层扩散
类比 树的先序遍历 树的层序遍历
典型工具 队列
常见实现 递归 queue
是否需要visited
是否适合无权图最短路 一般不保证
典型应用 搜索、连通块、回溯 最短步数、层次搜索

三十五、用一句话区分DFS和BFS

DFS

"一条路走到底!"

BFS

"一圈一圈往外扩!"

再记:

DFS

BFS

队列

基本就建立起来了。


三十六、CSP-J初赛重点考法

考法1:问BFS使用什么数据结构

答案:

队列


考法2:判断遍历顺序

看到:

复制代码
一层一层
逐层
层次遍历
由近及远

想到:

BFS

看到:

复制代码
深入
回退
递归
一路走到底

想到:

DFS


三十七、考法3:判断最短路

看到:

"每一步代价相同,求最少走几步。"

马上想到:

BFS

例如:

复制代码
迷宫最少走多少步?
棋盘最少移动多少次?
从A到B至少经过多少条边?

第一反应:

BFS


三十八、考法4:程序阅读

看到:

复制代码
queue<int> q;

然后:

复制代码
q.push(...)

和:

复制代码
q.front();
q.pop();

同时配合:

复制代码
visited[]

大概率就是:

BFS

而如果看到:

复制代码
dfs(...)

或者:

复制代码
stack<int>

则大概率是:

DFS


三十九、一道典型程序阅读题

复制代码
queue<int> q;

visited[0] = true;
q.push(0);

while (!q.empty())
{
    int v = q.front();
    q.pop();

    cout << v << " ";

    for (int u : g[v])
    {
        if (!visited[u])
        {
            visited[u] = true;
            q.push(u);
        }
    }
}

问:

该程序实现什么算法?

看到:

复制代码
queue

再看到:

复制代码
front
pop
push
visited

答案:

BFS


四十、再来一道"手算题"

图:

复制代码
          1
        /   \
       2     3
      / \   / \
     4   5 6   7

从1开始,邻接点按编号从小到大访问。

BFS:

第一层:

复制代码
1

第二层:

复制代码
2 3

第三层:

复制代码
4 5 6 7

所以:

1,2,3,4,5,6,7

这其实就是树的层序遍历。


四十一、再来一个容易错的图

复制代码
       0
      / \
     1   2
      \ /
       3

从0开始。

BFS:

复制代码
0

0发现:

复制代码
1、2

队列:

复制代码
1 → 2

处理1:

复制代码
1

发现3:

复制代码
2 → 3

处理2:

复制代码
2

发现3,但:

复制代码
visited[3] == true

所以不能重复加入。

最终:

0,1,2,3

这里最容易犯的错误是把3写两次。


四十二、为什么BFS特别适合"最少几步"?

可以把整个图按照距离分层:

复制代码
距离0:
        A

距离1:
     B     C

距离2:
   D   E     F

距离3:
      G

BFS的本质就是:

按照距离从小到大处理。

所以:

复制代码
第1次找到目标

意味着:

已经没有更短的办法了。

这就是BFS解决无权最短路的核心原因。


四十三、今天和前面知识的完整连接

到本课,我们已经形成了一条非常漂亮的知识链:

复制代码
                 图
                 │
          ┌──────┴──────┐
          │             │
       邻接矩阵       邻接表
          │             │
          └──────┬──────┘
                 │
            图的遍历
          ┌──────┴──────┐
          │             │
         DFS           BFS
          │             │
        深度优先       广度优先
          │             │
         栈            队列
          │             │
        递归          FIFO
          │             │
      一路深入       一层扩散

再往外延伸:

复制代码
DFS
 ├── 连通块
 ├── Flood Fill
 ├── 回溯
 └── 搜索

BFS
 ├── 连通块
 ├── Flood Fill
 ├── 迷宫
 └── 无权最短路

四十四、本课和上一课放在一起比较

大家完成这张"超级知识卡"。

问题 DFS BFS
怎么走? 先深入 先扩散
像什么? 先序遍历 层序遍历
用什么? 队列
递归常见吗? 很常见 不常用
需要visited吗? 需要 需要
无权最短路 不保证 非常适合
口诀 一条路走到底 一层一层向外扩

四十五、本课"必背知识卡"

① BFS是什么?

广度优先搜索(Breadth First Search)


② BFS的核心思想

一层一层向外扩展。


③ BFS像什么?

树的层序遍历。


④ BFS使用什么数据结构?

队列 Queue


⑤ 队列是什么特点?

FIFO------先进先出。


⑥ BFS需要什么数组?

复制代码
bool visited[N];

用于:

防止重复访问。


⑦ BFS最重要的代码结构

复制代码
queue<int> q;

visited[start] = true;
q.push(start);

while (!q.empty())
{
    int v = q.front();
    q.pop();

    for (int u : graph[v])
    {
        if (!visited[u])
        {
            visited[u] = true;
            q.push(u);
        }
    }
}

⑧ BFS的经典应用

无权图最短路

例如:

复制代码
迷宫最少几步
棋盘最少走几次
两个点最少经过多少条边

四十六、最后给大家一个"终极记忆法"

把DFS和BFS想成两种探险队。

DFS探险队

队长说:

"这条路不错,我们一直走进去!"

复制代码
A
↓
B
↓
D
↓
继续......

走不动:

复制代码
↑
回退

所以:

DFS = 深入 + 回退 + 栈


BFS探险队

队长说:

"大家不要乱跑!先把第一圈探索完,再探索第二圈!"

复制代码
       第二圈
    ○ ○ ○ ○ ○

       第一圈
      ○ ○ ○

        起点
         ●

所以:

BFS = 分层 + 扩散 + 队列


学生们要记住:

DFS问的是:"我能不能沿着这条路继续深入?"
BFS问的是:"离我最近的地方,我是不是已经全部探索完了?"


下一课 生成树与最小生成树(MST) ,我们提出一个非常有趣的问题:

如果一个网络有很多条道路,但我们不需要保留所有道路,只想让所有城市保持连通,而且总建设费用最低,该怎么办?

这就从"如何遍历图 "进入"如何建设图 ",并自然引出生成树、最小生成树、Prim、Kruskal


相关推荐
闻缺陷则喜何志丹1 小时前
【动态规划】P3609 [USACO17JAN] Hoof, Paper, Scissor G
c++·算法·动态规划·洛谷
汉克老师1 小时前
CSP-J 初赛(以满分为目标):第二十六课 《图的遍历——DFS深度优先搜索——从“树的先序遍历”走进真正的图世界》
c++·csp-j·小学生·学c++编程
程序猿编码1 小时前
基于GGML的C++17轻量化语音推理引擎:说话人识别与语音分析技术全解析
开发语言·c++·pytorch·深度学习·神经网络·大模型
影视飓风TIM2 小时前
C++哈希表深度剖析:冲突处理、rehash、迭代器与unordered容器封装
c++·哈希算法·散列表
C++ 老炮儿的技术栈2 小时前
Qt5 使用 QPainter 绘制阿基米德螺线
开发语言·c++·windows·qt·代码化
xxwxx__2 小时前
深入理解 C++ STL:stack、queue 与 deque 从使用到底层实现全解析
开发语言·c++·算法
wuminyu3 小时前
JVM锁膨胀与Futex源码解析
java·linux·c语言·jvm·c++
郝学胜-神的一滴3 小时前
C++11 工程级应用 08:Lambda表达式与Tuple元组
开发语言·jvm·c++·python·程序人生·开源
水饺编程4 小时前
第5章,[Win32 章节] :圆角矩形教学插图绘制程序
c语言·c++·windows·visual studio