文章目录
- [一、BFS 解决 FloodFill 算法](#一、BFS 解决 FloodFill 算法)
-
- [1. FloodFill 算法 的简介(如何用BFS实现)](#1. FloodFill 算法 的简介(如何用BFS实现))
- [2. 图像渲染(如何用代码实现BFS的过程)](#2. 图像渲染(如何用代码实现BFS的过程))
- [3. 岛屿数量](#3. 岛屿数量)
- [4. 练习题:岛屿的最大面积(思路同:岛屿数量)](#4. 练习题:岛屿的最大面积(思路同:岛屿数量))
- [5. 被围绕的区域(思想:正难则反)](#5. 被围绕的区域(思想:正难则反))
- [二、BFS 解决最短路问题](#二、BFS 解决最短路问题)
-
- [1. BFS 解决最短路问题的简介(含解题思路)](#1. BFS 解决最短路问题的简介(含解题思路))
- [2. 迷宫中离入口最近的出口](#2. 迷宫中离入口最近的出口)
- [3. 最小基因变化](#3. 最小基因变化)
- 三、多源BFS
-
- [1. 多源最短路问题的简介](#1. 多源最短路问题的简介)
- [2. 01 矩阵](#2. 01 矩阵)
- [3. 飞地的数量(思想:正难则反)](#3. 飞地的数量(思想:正难则反))
- [四、BFS 解决拓扑排序](#四、BFS 解决拓扑排序)
-
- [1. 拓扑排序简介](#1. 拓扑排序简介)
- [2. 课程表(用数据结构实现图,再进行拓扑排序)](#2. 课程表(用数据结构实现图,再进行拓扑排序))
- [3. 课程表 II](#3. 课程表 II)
一、BFS 解决 FloodFill 算法
1. FloodFill 算法 的简介(如何用BFS实现)
Flood Fill(泛洪填充)是一种经典的图像处理算法,其核心思想非常直观:从一个起始点出发,向四周扩散,将与起点颜色相同(或在容差范围内)的所有连通区域全部替换为新颜色。
(1) 核心原理与连通方式
算法通过检查当前像素的上下左右(或八个方向)邻居,判断其是否符合填充条件。其连通性主要分为两种:
-
四连通(4-directional): 只向上、下、左、右四个方向扩散。用于防止颜色"渗透"过仅对角线接触的缝隙。
-
八连通(8-directional): 向周围八个方向(含对角线)扩散。填充更彻底,但可能会"穿过"仅对角接触的角落。
(2) 实现方式:DFS 与 BFS
代码实现上,主要依赖两种搜索策略:
-
深度优先搜索(DFS,即递归/栈): 代码最简洁(递归)。找到一个点就立即深入其邻居,直到尽头再回溯。
- 缺点:在填充超大区域(如数万像素)时,递归层数过深极易导致栈溢出(Stack Overflow)。
-
广度优先搜索(BFS,即队列): 使用队列管理待处理的点。从起点开始,逐层向外"扫描"。
- 优点:避免了递归深度限制,处理大图像更安全、稳定。

2. 图像渲染(如何用代码实现BFS的过程)

示例 1:
输入:image = \[1,1,1,1,1,0,1,0,1],sr = 1, sc = 1, color = 2
输出:\[2,2,2,2,2,0,2,0,1]
解释:在图像的正中间,坐标 (sr,sc)=(1,1) (即红色像素),在路径上所有符合条件的像素点的颜色都被更改成相同的新颜色(即蓝色像素)。 注意,右下角的像素 没有更改为2,因为它不是在上下左右四个方向上与初始点相连的像素点。
示例 2:输入:image = \[0,0,0,0,0,0], sr = 0, sc = 0, color = 0
输出:\[0,0,0,0,0,0]
解释:初始像素已经用 0 着色,这与目标颜色相同。因此,不会对图像进行任何更改。
提示:m == image.length
n == imagei.length
1 <= m, n <= 50
0 <= imageij, color < 216
0 <= sr < m
0 <= sc < n

- 代码实现:
cpp
class Solution {
public:
int in_x[4] = {0, 0, 1, -1};
int in_y[4] = {1, -1, 0, 0};
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
int img_x = image.size();
int img_y = image[0].size();
int init_color = image[sr][sc];
if(init_color == color)
return image;
// 存储元素下标的 数列
queue< pair<int, int> > que_index;
que_index.push({sr, sc});
image[sr][sc] = color;
// 循环进行 BFS搜索
while(!que_index.empty())
{
pair<int, int> pa_xy = que_index.front();
que_index.pop();
for(int i = 0; i < 4; i++)
{
int x = pa_xy.first + in_x[i];
int y = pa_xy.second + in_y[i];
if(x >= 0 && x < img_x && y >= 0 && y < img_y && image[x][y] == init_color)
{
que_index.push({x, y});
image[x][y] = color;
}
}
}
return image;
}
};
3. 岛屿数量

示例 1:
**输入:
grid = \['1','1','1','1','0',
'1','1','0','1','0',
'1','1','0','0','0','0','0','0','0','0'
]**
输出:1
示例 2:**输入:
grid = \['1','1','0','0','0',
'1','1','0','0','0',
'0','0','1','0','0','0','0','0','1','1'
]**
输出:3
提示:m == grid.length
n == gridi.length
1 <= m, n <= 300
gridij 的值为 '0' 或 '1'

- 代码实现(采用解法一的思想):
cpp
class Solution {
public:
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
// 记录岛屿数量
int count = 0;
// 存储元素下标的 数列
queue< pair<int, int> > que_index;
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
// 发现 '1', 进行BFS,找出'1' 的连通块,找的过程中,将找过的 '1' 置为 '0'
if(grid[i][j] == '1')
{
count++;
que_index.push({i, j});
grid[i][j] = '0';
while(!que_index.empty())
{
pair<int, int> pa_xy = que_index.front();
que_index.pop();
for(int k = 0; k < 4; k++)
{
int x = pa_xy.first + dx[k];
int y = pa_xy.second + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1')
{
que_index.push({x, y});
grid[x][y] = '0';
}
}
}
}
}
}
return count;
}
};
4. 练习题:岛屿的最大面积(思路同:岛屿数量)


5. 被围绕的区域(思想:正难则反)


- 代码实现:
cpp
class Solution {
public:
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
void BFS(vector<vector<char>>& _board, vector<vector<bool>>& _vec_bool, int _i, int _j)
{
int m = _board.size();
int n = _board[0].size();
queue<pair<int, int>> que_index;
que_index.push({_i, _j});
_vec_bool[_i][_j] = true;
while(!que_index.empty())
{
pair<int, int> pa_xy = que_index.front();
que_index.pop();
for(int i = 0; i < 4; i++)
{
int x = pa_xy.first + dx[i];
int y = pa_xy.second + dy[i];
if(x >= 0 && x < m && y >= 0 && y < n && _board[x][y] == 'O' && _vec_bool[x][y] == 0)
{
que_index.push({x, y});
_vec_bool[x][y] = true;
}
}
}
}
void solve(vector<vector<char>>& board) {
int m = board.size();
int n = board[0].size();
vector<vector<bool>> vec_bool(m, vector<bool>(n, false));
// 先利⽤ bfs 将与边缘相连的 '0' 区域做上标记(将对应位置的bool修改为 1)
for(int i = 0; i < m; i++)
{
if(i == 0 || i == m-1) // 只有第一排 和 最后一排遍历整排
{
for(int j = 0; j < n; j++)
{
if(board[i][j] == 'O' && vec_bool[i][j] == 0)
BFS(board, vec_bool, i, j);
}
}
else // 非第一排 和 最后一排只遍历该排首尾元素
{
if(board[i][0] == 'O' && vec_bool[i][0] == 0)
BFS(board, vec_bool, i, 0);
if(board[i][n-1] == 'O' && vec_bool[i][n-1] == 0)
BFS(board, vec_bool, i, n-1);
}
}
// 将没有标记过的 '0'(对应位置的bool值仍然是false 的'0') 修改成 'X' 即可
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(board[i][j] == 'O' && vec_bool[i][j] == 0)
board[i][j] = 'X';
}
}
}
};
二、BFS 解决最短路问题
1. BFS 解决最短路问题的简介(含解题思路)
针对边权均为1(即边权全都相同,每两点间的距离都相同)的最短路问题,广度优先搜索(BFS) 是绝对最优解。以下是其核心简介:
(1) 核心原理:队列与"波纹效应"
BFS 利用队列(FIFO) 的先进先出特性,从起点开始,像水波一样逐层向外扩散。
-
第 1 层:距离起点为 1 的所有节点;
-
第 2 层:距离起点为 2 的所有节点;
-
......
由于队列保证了"先访问到的节点,其距离一定不大于后访问到的节点",因此 当某个节点第一次被访问(入队)时,所走的路径一定是从起点到该节点的最短路径(即第一次命中即为最优解)。
(2) 关键注意事项(避坑指南)
必须判重(标记访问):若不加 哈希表hash(或者 其它能用来标记访问的数据结构),节点会重复入队,导致死循环或指数级复杂度。标记时机必须是入队时(而非出队时),否则同一节点可能被多次入队,破坏距离最短性。

2. 迷宫中离入口最近的出口




- 代码实现:
cpp
class Solution {
public:
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int nearestExit(vector<vector<char>>& maze, vector<int>& entrance) {
int m = maze.size();
int n = maze[0].size();
// 存储元素下标的数列
queue< pair<int, int> > que;
// 标记该位置是否被搜索过的bool数组
vector<vector<bool>> vec_bool(m, vector<bool>(n, false));
// 准备阶段
que.push({entrance[0], entrance[1]});
vec_bool[entrance[0]][entrance[1]] = true;
int number = 0; // 记录BFS的搜索轮次
// 进行多轮BFS
while(!que.empty())
{
// 计算该轮 要进行几次BFS
int count = que.size();
number++;
while(count--)
{
pair<int, int> pa_xy = que.front();
que.pop();
for(int i = 0; i < 4; i++)
{
int x = pa_xy.first + dx[i];
int y = pa_xy.second + dy[i];
if(x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == '.' && vec_bool[x][y] == false)
{
// 到达出口,返回最短路径(也就是BFS轮数)
if(x == 0 || x == m-1 || y == 0 || y == n-1)
return number;
que.push({x, y});
vec_bool[x][y] = true;
}
}
}
}
return -1;
}
};
3. 最小基因变化


- 代码实现:
cpp
class Solution {
public:
// 比较两个字符串 是否只有一个字符的差异
bool com_string(const string &str1, const string &str2)
{
int count = 0;
for(int i = 0; i < 8; i++)
{
if(str1[i] != str2[i])
count++;
}
if(count == 1)
return true;
return false;
}
int minMutation(string startGene, string endGene, vector<string>& bank) {
if(startGene == endGene)
return 0;
int m = bank.size();
// 存储字符串的 队列
queue<string> que;
// 记录基因库每个位置是否被搜索过
vector<bool> vec_bool(m, false);
// 将起始字符串 入队
que.push(startGene);
// 记录BFS的轮次
int number = 0;
// 进行多轮BFS
while(!que.empty())
{
number++;
// 计算该轮 要进行多少次BFS
int count = que.size();
while(count--)
{
string tmp = que.front();
que.pop();
// 找出所有基因库bank中一次变换能变成的 字符串
for(int i = 0; i < m; i++)
{
if(vec_bool[i] == false && com_string(tmp, bank[i]))
{
if(bank[i] == endGene) // 最初字符串变换到endGene时,BFS的轮次就是最少变化次数
return number;
que.push(bank[i]);
vec_bool[i] = true;
}
}
}
}
return -1;
}
};
三、多源BFS
1. 多源最短路问题的简介
针对边权均为1的多源最短路问题(即有多个起点),BFS 同样是最优解。其核心思想是"多源头同时点火"。
(1) 核心原理:虚拟超级源点
多源 BFS 不需要对每个起点分别跑一次 BFS(那样会超时)。它的技巧是在逻辑上创建一个"虚拟超级源点":
-
将这个虚拟源点用 0 权值的边连接到所有真实的起点。
-
从虚拟源点开始跑单源 BFS。
落实到代码中更简单:初始化队列时,一次性将所有起点全部入队。 这样,BFS 的"波纹"从所有起点同时向外扩散,谁先扩散到某个节点,该节点到最近起点的距离就是最短的。
(2) 关键注意事项(避坑指南)
必须判重(标记访问):若不加 哈希表hash(或者 其它能用来标记访问的数据结构),节点会重复入队,导致死循环或指数级复杂度。标记时机必须是入队时(而非出队时),否则同一节点可能被多次入队,破坏距离最短性。
2. 01 矩阵



- 代码示例:
cpp
class Solution {
public:
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
int m = mat.size();
int n = mat[0].size();
// 存储元素下标的队列
queue<pair<int, int>> que;
// 标记元素为1的位置是否被搜索过的 bool数组
vector<vector<bool>> vec_bool(m, vector<bool>(n, false));
// 记录每个位置到最近0的距离
vector<vector<int>> distance(m, vector<int>(n, 0));
// 遍历数组,把所有0元素的位置坐标入队列
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(mat[i][j] == 0)
que.push({i, j});
}
}
// 记录BFS的轮次
int number = 0;
// 进行多轮BFS
while(!que.empty())
{
number++;
// 记录此轮 进行多少次BFS
int count = que.size();
while(count--)
{
pair<int, int> pa_xy = que.front();
que.pop();
for(int i = 0; i < 4; i++)
{
int x = pa_xy.first + dx[i];
int y = pa_xy.second + dy[i];
if(x >= 0 && x < m && y >= 0 && y < n && mat[x][y] == 1 && vec_bool[x][y] == false)
{
que.push({x, y});
vec_bool[x][y] = true;
distance[x][y] = number; // 该轮找到的元素1,它们到最近0的距离 就是BFS的轮次
}
}
}
}
return distance;
}
};
3. 飞地的数量(思想:正难则反)


- 代码演示:
cpp
class Solution {
public:
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int numEnclaves(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
queue<pair<int, int>> que;
vector<vector<bool>> vec_bool(m, vector<bool>(n, false));
// 1. 把边缘上的 1 加⼊到队列中
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(i == 0 || i == m - 1 || j == 0 || j == n - 1)
{
if(grid[i][j] == 1)
{
que.push({i, j});
vec_bool[i][j] = true;
}
}
}
}
// 2. 多源 bfs(将所有与边缘 1 联通的1标记为true)
while(!que.empty())
{
pair<int, int> pa_xy = que.front();
que.pop();
for(int i = 0; i < 4; i++)
{
int x = pa_xy.first + dx[i];
int y = pa_xy.second + dy[i];
if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1 && vec_bool[x][y] == false)
{
que.push({x, y});
vec_bool[x][y] = true;
}
}
}
// 3. 遍历数组,统计所有不与边缘1 联通的1
int number = 0;
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(grid[i][j] == 1 && vec_bool[i][j] == false)
number++;
}
}
return number;
}
};
四、BFS 解决拓扑排序
1. 拓扑排序简介
(1) 有向无环图(DAG图)
-
入度(入度 - Indegree): 进入一个顶点的边的数量。它代表前置依赖项的数量(在拓扑排序中,依赖项在之前完成)。
-
出度(出度 - Outdegree): 离开一个顶点的边的数量。它代表依赖于当前顶点的后续任务的数量。

(2) AOV网(Activity On Vertex Network,顶点表示活动的网络)就是你刚才一直在讨论的拓扑排序的"应用载体"。
简单来说:把有向无环图(DAG)用于描述工程流程时,这张图就叫做 AOV 网。

为了帮你彻底打通概念,我们把 AOV 网和之前聊的入度/出度、拓扑排序串在一起:
- AOV 网的核心定义(核心理念)
顶点(Vertex): 表示一个活动(Activity)或任务(如"打地基"、"安装门窗")。
有向边(Edge): 表示活动之间的先后依赖关系。如果存在边 A → B,就意味着 "A 必须在 B 之前完成"。
注意: AOV 网必须是有向无环图(DAG)。一旦出现环,就意味着活动之间产生了"死锁"式的循环依赖(A等B,B等C,C等A),工程永远无法开工。
- 入度/出度在 AOV 网中的"工程含义"
把抽象的图论术语翻译成项目管理的语言,你会瞬间明白它们的实际价值:
| 图论术语 | AOV 网中的工程含义 | 在项目中的角色 |
|---|---|---|
| 入度(Indegree) | 该活动紧前任务(前置依赖)的数量。 | 入度 = 0 的活动,代表没有前置条件,可以立即开工(如项目启动会)。 |
| 出度(Outdegree) | 该活动紧后任务(后续依赖)的数量。 | 出度 = 0 的活动,代表没有后续环节,是整个项目的收尾节点(如项目验收)。 |
在 Kahn 算法中,每完成一个入度为 0 的活动,就相当于"该活动已完工",此时将它所有后继活动的入度减 1(表示解除了对后继活动的唯一阻碍)。
(3)拓扑排序
找出做事情的先后顺序,拓扑排序的结果可能不是唯一的
拓扑排序的本质,就是将有向无环图(DAG)中的所有顶点排成一个线性序列
- 如何排序?
- (1)找出图中入度为 0 的点,然后输出
- (2)删除与该点连接的边
- (3)重复 1、2 操作,直到图为空(图中没有点) 或者 图中没有入度为0的点为止(图中存在环)

(4)拓扑排序可以用来检测一个有向图是否存在环(有环的话,拓扑排序就无法完成)

2. 课程表(用数据结构实现图,再进行拓扑排序)



- 代码实现:
cpp
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
// 1. 建图
// (1)邻接表(key: 课程 ; value: key出度指向的所有课程)
unordered_map<int, vector<int>> edges;
// (2)记录每个点的入度数量(初始每个点的入度数量为0)
unordered_map<int, int> in;
for(int i = 0; i < numCourses; i++)
{
in.insert({i, 0});
}
for(auto it : prerequisites)
{
// 假设 it为[0, 1], 代表0 依赖 1,也就是1 -> 0
// 1这个点 出度方向的边指向0
auto ret = edges.find(it[1]);
if(ret == edges.end())
{
edges.insert({it[1], vector<int>(1, it[0])});
}
else
ret->second.push_back(it[0]);
// 0这个点的入度+1
in[it[0]]++;
}
// 2. 进行拓扑排序(判断有没有环)
int count = 0; // 记录BFS的次数
// 存储入度为0的 课程
queue<int> que;
for(auto it : in)
{
if(it.second == 0)
que.push(it.first);
}
// 循环进行BFS
while(!que.empty())
{
count++;
// 取出队头元素
int tmp = que.front();
que.pop();
// 查邻接表,将与队头元素相连的顶点的入度-1
auto tmp_vec = edges.find(tmp);
if(tmp_vec != edges.end())
{
for(auto it : tmp_vec->second)
{
in[it]--;
// 判断相连的顶点的入度是否减成0了,
// 如果减成 0,将相连的顶点 入队列
if(in[it] == 0)
que.push(it);
}
}
}
// 3. 判断 count 的次数
if(count == numCourses) // 对numCourses门课程 都进行了拓扑排序
return true;
else // 图中存在环,无法对所有课程都进行拓扑排序
return false;
}
};
3. 课程表 II

- 代码解析:
cpp
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites)
{
// 1. 建图
// (1)邻接表(key: 课程 ; value: key出度指向的所有课程)
unordered_map<int, vector<int>> edges;
// (2)记录每个点的入度数量(初始每个点的入度数量为0)
unordered_map<int, int> in;
for(int i = 0; i < numCourses; i++)
{
in.insert({i, 0});
}
for(auto it : prerequisites)
{
// 假设 it为[0, 1], 代表0 依赖 1,也就是1 -> 0
// 1这个点 出度方向的边指向0
auto ret = edges.find(it[1]);
if(ret == edges.end())
{
edges.insert({it[1], vector<int>(1, it[0])});
}
else
ret->second.push_back(it[0]);
// 0这个点的入度+1
in[it[0]]++;
}
// 2. 进行拓扑排序(判断有没有环)
int count = 0; // 记录BFS的次数
// 存储入度为0的 课程
queue<int> que;
for(auto it : in)
{
if(it.second == 0)
que.push(it.first);
}
// 存储拓扑排序的结果
vector<int> sort_result;
// 循环进行BFS
while(!que.empty())
{
count++;
// 取出队头元素
int tmp = que.front();
que.pop();
sort_result.push_back(tmp);
// 查邻接表,将与队头元素相连的顶点的入度-1
auto tmp_vec = edges.find(tmp);
if(tmp_vec != edges.end())
{
for(auto it : tmp_vec->second)
{
in[it]--;
// 判断相连的顶点的入度是否减成0了,
// 如果减成 0,将相连的顶点 入队列
if(in[it] == 0)
que.push(it);
}
}
}
// 3. 判断 count 的次数
if(count == numCourses) // 对numCourses门课程 都进行了拓扑排序
return sort_result;
else // 图中存在环,无法对所有课程都进行拓扑排序
return vector<int>();
}
};
