图的基本概念 (从代码随想录学习 + 自己的理解)
二维坐标中,两点可以连成线,多个点连成的线就构成了图。
当然图也可以就一个节点,甚至没有节点(空图)
整体上一般分为 有向图 和 无向图。
有向图是指 图中边是有方向的:
无向图是指 图中边没有方向:
加权有向图,就是图中边是有权值的,例如:
加权无向图也是同理。
无向图中有几条边连接该节点,该节点就有几度。
在有向图中,每个节点有出度和入度。
出度:从该节点出发的边的个数。
入度:指向该节点边的个数。
在图中表示节点的连通情况,我们称之为连通性。
在无向图中,任何两个节点都是可以到达的,我们称之为连通图 ,
如果有节点不能到达其他节点,则为非连通图,
在有向图中,任何两个节点是可以相互到达的,我们称之为 强连通图。
和无向图中的连通图有什么区别,不是一样的吗?
强连通图是在有向图中任何两个节点是可以相互到达
在无向图中的极大连通子图称之为该图的一个连通分量。
在图论中,连通分量是一个很重要的概念,例如岛屿问题(后面章节会有专门讲解)其实就是求连通分量。
在有向图中极大强连通子图称之为该图的强连通分量。
我们如何用代码来表示一个图呢?
一般使用邻接表、邻接矩阵 或者用类来表示。
主要是 朴素存储、邻接表和邻接矩阵。
邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。
例如: grid[ 2 ] [ 5 ] = 6 ,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5 ,边的权值为6 。
如果想表示无向图,即:grid[ 2 ] [ 5 ] = 6 ,grid[ 5 ] [ 2 ] = 6 ,表示节点2 与 节点5 相互连通,权值为6 。
在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间。
图中有一条双向边,即:grid[ 2 ] [ 5 ] = 6 ,grid[ 5 ] [ 2 ] = 6
这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。
而且在寻找节点连接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。
邻接矩阵的优点:
表达方式简单,易于理解
检查任意两个顶点间是否存在边的操作非常快
适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。
缺点:
遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费
邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。
这里表达的图是:
节点1 指向 节点3 和 节点5
节点2 指向 节点4 、节点3 、节点5
节点3 指向 节点4
节点4 指向节点1
有多少边 邻接表才会申请多少个对应的链表节点。
从图中可以直观看出 使用 数组 + 链表 来表达 边的连接情况 。
邻接表的优点:
对于稀疏图的存储,只需要存储边,空间利用率高
遍历节点连接情况相对容易
缺点:
检查任意两个节点间是否存在边,效率相对低,需要 O( V) 时间,V表示某节点连接其他节点的数量。
实现相对复杂,不易理解
以上大家可能理解比较模糊,没关系,因为大家还没做过图论的题目,对于图的表达没有概念。
这里我先不给出具体的实现代码,大家先有个初步印象,在后面算法题实战中,我还会讲到具体代码实现,等带大家做算法题,写了代码之后,自然就理解了。
图的遍历方式基本是两大类:
深度优先搜索(dfs)
广度优先搜索(bfs)
在讲解二叉树章节的时候,其实就已经讲过这两种遍历方式。
二叉树的递归遍历,是dfs 在二叉树上的遍历方式。
二叉树的层序遍历,是bfs 在二叉树上的遍历方式。
dfs 和 bfs 一种搜索算法,可以在不同的数据结构上进行搜索,在二叉树章节里是在二叉树这样的数据结构上搜索。
而在图论章节,则是在图(邻接表或邻接矩阵)上进行搜索。
以上知识点 大家先有个印象,上面提到的每个知识点,其实都需要大篇幅才能讲明白的。
我这里先给大家做一个概括,后面章节会针对每个知识点都会有对应的算法题和针对性的讲解,大家再去深入学习。
图论是非常庞大的知识体系,上面的内容还不足以概括图论内容,仅仅是理论基础而已。
在图论章节我会带大家深入讲解 深度优先搜索(DFS)、广度优先搜索(BFS)、并查集、拓扑排序、最小生成树系列、最短路算法系列等等。
深度优先搜索理论基础
提到深度优先搜索(dfs),就不得不说和广度优先搜索(bfs)有什么区别
dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。
上面说道dfs是可一个方向搜,不到黄河不回头。
搜索方向,是认准一个方向搜,直到碰壁之后再换方向
换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。
正是因为dfs搜索可一个方向,并需要回溯,所以用递归的方式来实现是最方便的。
很多录友对回溯很陌生,建议先看看代码随想录,回溯算法章节。
有递归的地方就有回溯,那么回溯在哪里呢?
# 就递归函数的下面,例如如下代码:
void dfs ( 参数) {
处理节点
dfs ( 图,选择的节点) ;
回溯,撤销处理结果
}
可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的。
在讲解二叉树章节的时候,二叉树的递归法其实就是dfs,而二叉树的迭代法,就是bfs(广度优先搜索)
所以dfs,bfs其实是基础搜索算法,也广泛应用与其他数据结构与算法中。
我们在回顾一下回溯法的代码框架:
void backtracking ( 参数) {
if ( 终止条件) {
存放结果;
return ;
}
for ( 选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking ( 路径,选择列表) ;
回溯,撤销处理结果
}
}
回溯算法,其实就是dfs的过程,这里给出dfs的代码框架:
void dfs ( 参数) {
if ( 终止条件) {
存放结果;
return ;
}
for ( 选择:本节点所连接的其他节点) {
处理节点;
dfs ( 图,选择的节点) ;
回溯,撤销处理结果
}
}
可以发现dfs的代码框架和回溯算法的代码框架是差不多的。
下面我在用 深搜三部曲,来解读 dfs的代码框架。
在 二叉树递归讲解中,给出了递归三部曲。
回溯算法讲解中,给出了 回溯三部曲。
其实深搜也是一样的,深搜三部曲如下:
确认递归函数,参数
void dfs( 参数)
通常我们递归的时候,我们递归搜索需要了解哪些参数,其实也可以在写递归函数的时候,发现需要什么参数,再去补充就可以。
一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。
例如这样:
vector< vector< int >> result; // 保存符合条件的所有路径
vector< int > path; // 起点到终点的路径
void dfs ( 图,目前搜索的节点)
但这种写法看个人习惯,不强求。
确认终止条件
终止条件很重要,很多同学写dfs的时候,之所以容易死循环,栈溢出等等这些问题,都是因为终止条件没有想清楚。
if ( 终止条件) {
存放结果;
return ;
}
终止添加不仅是结束本层递归,同时也是我们收获结果的时候。
另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。这里如果大家不理解的话,没关系,后面会有具体题目来讲解。
处理目前搜索节点出发的路径
一般这里就是一个for 循环的操作,去遍历 目前搜索节点 所能到的所有节点。
for ( 选择:本节点所连接的其他节点) {
处理节点;
dfs( 图,选择的节点) ; // 递归
回溯,撤销处理结果
}
98. 所有可达路径
卡码网题目链接(ACM模式)
【题目描述】
给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。
【输入描述】
第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边
后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径
【输出描述】
输出所有的可达路径,路径中所有节点的后面跟一个空格,每条路径独占一行,存在多条路径,路径输出的顺序可任意。
如果不存在任何一条路径,则输出 - 1 。
注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 1 3 5 , 而不是 1 3 5 , 5 后面没有空格!
5 3
1 3
2 3
1 5
数据范围:
图中不存在自环
图中不存在平行边
1 <= N <= 100
1 <= M <= 500
思考
本题和力扣 797. 所有可能的路径 ( opens new window) 是一样的,录友了解深度优先搜索之后,这道题目就是模板题,是送分题。
力扣是核心代码模式,把图的存储方式给大家定义好了,只需要写出深搜的核心代码就可以。
如果笔试的时候出一道原题 (笔试都是ACM模式,部分面试也是ACM模式),不少熟练刷力扣的录友都难住了,因为不知道图应该怎么存,也不知道自己存的图如何去遍历。
所以这也是为什么我要让大家练习 ACM模式
这道题目是深度优先搜索,比较好的入门题。
我依然总结了深搜三部曲,如果按照代码随想录刷题的录友,应该刷过 二叉树的递归三部曲,回溯三部曲。
大家可能有疑惑,深搜 和 二叉树和回溯算法 有什么区别呢? 什么时候用深搜 什么时候用回溯?
我在讲解二叉树理论基础的时候,提到过,二叉树的前中后序遍历其实就是深搜在二叉树这种数据结构上的应用。
那么回溯算法呢,其实 回溯算法就是 深搜,只不过针对某一搜索场景 我们给他一个更细分的定义,叫做回溯算法。
那有的录友可能说:那我以后称回溯算法为深搜,是不是没毛病?
理论上来说,没毛病,但 就像是 二叉树 你不叫它二叉树,叫它数据结构,有问题不? 也没问题对吧。
建议是 有细分的场景,还是称其细分场景的名称。 所以回溯算法可以独立出来,但回溯确实就是深搜。
在图论理论基础篇 中我们讲到了 两种 图的存储方式:邻接表 和 邻接矩阵。
本题我们将带大家分别实现这两个图的存储方式。
邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。
本题我们会有n 个节点,因为节点标号是从1 开始的,为了节点标号和下标对齐,我们申请 ( n + 1 ) * ( n + 1 ) 这么大的二维数组。
vector< vector< int >> graph( n + 1 , vector< int > ( n + 1 , 0 ) ) ;
输入m个边,构造方式如下:
while ( m- - ) {
cin >> s >> t;
// 使用邻接矩阵 ,1 表示 节点s 指向 节点t
graph[ s] [ t] = 1 ;
}
n, m = [ int ( v) for v in input ( ) . split( ' ' ) ]
graph = [ [ 0 for _ in range ( n+ 1 ) ] for _ in range ( n+ 1 ) ]
while m > 0 :
s, t = [ int ( v) for v in input ( ) . split( ' ' ) ]
graph[ s] [ t] = 1
m -= 1
邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。
邻接表的构造相对邻接矩阵难理解一些。
我在 图论理论基础篇 举了一个例子:
这里表达的图是:
节点1 指向 节点3 和 节点5
节点2 指向 节点4 、节点3 、节点5
节点3 指向 节点4
节点4 指向 节点1
我们需要构造一个数组,数组里的元素是一个链表。
1
5 3 4
2
C+ + 写法:
// 节点编号从1 到n,所以申请 n+ 1 这么大的数组
vector< list < int >> graph( n + 1 ) ; // 邻接表,list 为C+ + 里的链表
输入m个边,构造方式如下:
while ( m- - ) {
cin >> s >> t;
// 使用邻接表 ,表示 s - > t 是相连的
graph[ s] . push_back( t) ;
}
n, m = [ int ( v) for v in input ( ) . split( " " ) ]
graph = { v: [ ] for v in range ( n+ 1 ) }
while m:
m -= 1
s, t = [ int ( v) for v in input ( ) . split( " " ) ]
graph[ s] . append( t)
本题我们使用邻接表 或者 邻接矩阵都可以,因为后台数据并没有对图的大小以及稠密度做很大的区分。
以下我们使用邻接矩阵的方式来讲解,文末我也会给出 使用邻接表的整体代码。
注意邻接表 和 邻接矩阵的写法都要掌握!
本题是深度优先搜索的基础题目,关于深搜我在图论深搜理论基础 已经有详细的讲解,图文并茂。
关于本题我会直接使用深搜三部曲来分析,如果对深搜不够了解,建议先看 图论深搜理论基础。
深搜三部曲来分析题目:
确认递归函数,参数
首先我们dfs函数一定要存一个图,用来遍历的,需要存一个目前我们遍历的节点,定义为x。
还需要存一个n,表示终点,我们遍历的时候,用来判断当 x== n 时候 标明找到了终点。
(其实在递归函数的参数 不容易一开始就确定了,一般是在写函数体的时候发现缺什么,参加就补什么)
至于 单一路径 和 路径集合 可以放在全局变量,那么代码是这样的:
vector< vector< int >> result; // 收集符合条件的路径
vector< int > path; // 0 节点到终点的路径
// x:目前遍历的节点
// graph:存当前的图
// n:终点
void dfs ( const vector< vector< int >> & graph, int x, int n) {
确认终止条件
什么时候我们就找到一条路径了?
当目前遍历的节点 为 最后一个节点 n 的时候 就找到了一条 从出发点到终止点的路径。
// 当前遍历的节点x 到达节点n
if ( x == n) { // 找到符合条件的一条路径
result. push_back( path) ;
return ;
}
if i == n:
result. append( path[ : ] )
处理目前搜索节点出发的路径
接下来是走 当前遍历节点x的下一个节点。
首先是要找到 x节点指向了哪些节点呢? 遍历方式是这样的:
for ( int i = 1 ; i <= n; i+ + ) { // 遍历节点x链接的所有节点
if ( graph[ x] [ i] == 1 ) { // 找到 x指向的节点,就是节点i
}
}
for i in range ( 1 , n+ 1 ) :
if graph[ x] [ i] = 1
接下来就是将 选中的x所指向的节点,加入到 单一路径来。
path. push_back( i) ; // 遍历到的节点加入到路径中来
path. append( i)
进入下一层递归
dfs( graph, i, n) ; // 进入下一层递归, 以 i 为起点,寻找 i 指向的节点。
最后就是回溯的过程,撤销本次添加节点的操作。
为什么要有回溯,我在图论深搜理论基础 也有详细的讲解。 撤销当前的操作。
该过程整体代码:
for ( int i = 1 ; i <= n; i+ + ) { // 遍历节点x链接的所有节点
if ( graph[ x] [ i] == 1 ) { // 找到 x链接的节点
path. push_back( i) ; // 遍历到的节点加入到路径中来
dfs( graph, i, n) ; // 进入下一层递归
path. pop_back( ) ; // 回溯,撤销本节点
}
}
for i in range ( 1 , n+ 1 ) :
if graph[ x] [ i] == 1 :
path. append( i)
dfs( graph, i, n)
path. pop( )
ACM格式大家在输出结果的时候,要关注看看格式问题,特别是字符串,有的题目说的是每个元素后面都有空格,有的题目说的是 每个元素间有空格,最后一个元素没有空格。
有的题目呢,压根没说,那只能提交去试一试了。
很多录友在提交题目的时候发现结果一样,为什么提交就是不对呢。
例如示例输出是:
1 3 5 而不是 1 3 5
即 5 的后面没有空格!
这是我们在输出的时候需要注意的点。
有录友可能会想,ACM格式就是麻烦,有空格没有空格有什么影响,结果对了不就行了?
ACM模式相对于核心代码模式(力扣) 更考验大家对代码的掌控能力。 例如工程代码里,输出输出都是要自己控制的。这也是为什么大公司笔试,都是ACM模式。
以上代码中,结果都存在了 result数组里(二维数组,每一行是一个结果),最后将其打印出来。(重点看注释)
// 输出结果
if ( result. size( ) == 0 ) cout << - 1 << endl;
for ( const vector< int > & pa : result) {
for ( int i = 0 ; i < pa. size( ) - 1 ; i+ + ) { // 这里指打印到倒数第二个
cout << pa[ i] << " " ;
}
cout << pa[ pa. size( ) - 1 ] << endl; // 这里再打印倒数第一个,控制最后一个元素后面没有空格
}
lens = len ( result)
if lens == 0 : print ( - 1 )
for v in result:
for i in v[ : - 1 ] :
print ( i, end = " " )
print ( v[ - 1 ] )
本题代码
邻接矩阵写法 code c++
# include <iostream>
# include <vector>
using namespace std;
vector< vector< int >> result;
vector< int > path;
void dfs ( const vector< vector< int >> & graph, int x, int n) {
if ( x == n) {
result. push_back ( path) ;
return ;
}
for ( int i = 1 ; i <= n; i++ ) {
if ( graph[ x] [ i] == 1 ) {
path. push_back ( i) ;
dfs ( graph, i, n) ;
path. pop_back ( ) ;
}
}
}
int main ( ) {
int n, m, s, t;
cin >> n >> m;
vector< vector< int >> graph ( n + 1 , vector < int > ( n + 1 , 0 ) ) ;
while ( m-- ) {
cin >> s >> t;
graph[ s] [ t] = 1 ;
}
path. push_back ( 1 ) ;
dfs ( graph, 1 , n) ;
if ( result. size ( ) == 0 ) cout << - 1 << endl;
for ( const vector< int > & pa : result) {
for ( int i = 0 ; i < pa. size ( ) - 1 ; i++ ) {
cout << pa[ i] << " " ;
}
cout << pa[ pa. size ( ) - 1 ] << endl;
}
}
邻接矩阵写法 code python
def dfs ( graph, result, path, x, n) :
if x == n:
result. append( path[ : ] )
return
for i in range ( 1 , n+ 1 ) :
if graph[ x] [ i] == 1 :
path. append( i)
dfs( graph, result, path, i, n)
path. pop( )
def main ( ) :
n, m = [ int ( v) for v in input ( ) . split( " " ) ]
graph = [ [ 0 for _ in range ( n+ 1 ) ] for _ in range ( n+ 1 ) ]
while m:
m -= 1
s, t = [ int ( v) for v in input ( ) . split( " " ) ]
graph[ s] [ t] = 1
result = [ ]
path = [ 1 ]
dfs( graph, result, path, 1 , n)
lens = len ( result)
if lens == 0 : print ( - 1 )
for v in result:
for i in v[ : - 1 ] :
print ( i, end= " " )
print ( v[ - 1 ] )
main( )
邻接表写法 code c++
# include <iostream>
# include <vector>
# include <list>
using namespace std;
vector< vector< int >> result;
vector< int > path;
void dfs ( const vector< list< int >> & graph, int x, int n) {
if ( x == n) {
result. push_back ( path) ;
return ;
}
for ( int i : graph[ x] ) {
path. push_back ( i) ;
dfs ( graph, i, n) ;
path. pop_back ( ) ;
}
}
int main ( ) {
int n, m, s, t;
cin >> n >> m;
vector< list< int >> graph ( n + 1 ) ;
while ( m-- ) {
cin >> s >> t;
graph[ s] . push_back ( t) ;
}
path. push_back ( 1 ) ;
dfs ( graph, 1 , n) ;
if ( result. size ( ) == 0 ) cout << - 1 << endl;
for ( const vector< int > & pa : result) {
for ( int i = 0 ; i < pa. size ( ) - 1 ; i++ ) {
cout << pa[ i] << " " ;
}
cout << pa[ pa. size ( ) - 1 ] << endl;
}
}
邻接表写法 code python
def dfs ( graph, result, path, x, n) :
if x == n:
result. append( path[ : ] )
return
for v in graph[ x] :
path. append( v)
dfs( graph, result, path, v, n)
path. pop( )
def main ( ) :
n, m = [ int ( v) for v in input ( ) . split( " " ) ]
graph = [ [ ] for _ in range ( n+ 1 ) ]
while m:
m -= 1
s, t = [ int ( v) for v in input ( ) . split( " " ) ]
graph[ s] . append( t)
result = [ ]
path = [ 1 ]
dfs( graph, result, path, 1 , n)
lens = len ( result)
if lens == 0 : print ( - 1 )
for v in result:
for i in v[ : - 1 ] :
print ( i, end= " " )
print ( v[ - 1 ] )
main( )
总结
本题是一道简单的深搜题目,也可以说是模板题,和 力扣797. 所有可能的路径 ( opens new window) 思路是一样一样的。
很多录友做力扣的时候,轻松就把代码写出来了, 但做面试笔试的时候,遇到这样的题就写不出来了。
在力扣上刷题不用考虑图的存储方式,也不用考虑输出的格式。
而这些都是 ACM 模式题目的知识点(图的存储方式)和细节(输出的格式)
所以我才会特别制作ACM题目,同样也重点去讲解图的存储和遍历方式,来帮大家去练习。
对于这种有向图路径问题,最合适使用深搜,当然本题也可以使用广搜,但广搜相对来说就麻烦了一些,需要记录一下路径。
而深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+ 标记,所以使用哪种遍历都是可以的。
广度优先搜索理论基础
广搜(bfs)是一圈一圈的搜索过程;
深搜(dfs)是一条路跑到黑然后再回溯;
广搜的搜索方式就适合于解决两个点之间的最短路径问题。
因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行。
大家应该好奇,这一圈一圈的搜索过程是怎么做到的,是放在什么容器里,才能这样去遍历。
很多网上的资料都是直接说用队列来实现。
其实,我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,那么用队列,还是用栈,甚至用数组,都是可以的。
用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针。
因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。
如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历。
因为栈是先进后出,加入元素和弹出元素的顺序改变了。
那么广搜需要注意 转圈搜索的顺序吗? 不需要!
所以用队列,还是用栈都是可以的,但大家都习惯用队列了,所以下面的讲解用我也用队列来讲,只不过要给大家说清楚,并不是非要用队列,用栈也可以。
下面给出广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释)
code c++
int dir[ 4 ] [ 2 ] = { 0 , 1 , 1 , 0 , - 1 , 0 , 0 , - 1 } ;
void bfs ( vector< vector< char >> & grid, vector< vector< bool >> & visited, int x, int y) {
queue< pair< int , int >> que;
que. push ( { x, y} ) ;
visited[ x] [ y] = true ;
while ( ! que. empty ( ) ) {
pair< int , int > cur = que. front ( ) ; que. pop ( ) ;
int curx = cur. first;
int cury = cur. second;
for ( int i = 0 ; i < 4 ; i++ ) {
int nextx = curx + dir[ i] [ 0 ] ;
int nexty = cury + dir[ i] [ 1 ] ;
if ( nextx < 0 || nextx >= grid. size ( ) || nexty < 0 || nexty >= grid[ 0 ] . size ( ) ) continue ;
if ( ! visited[ nextx] [ nexty] ) {
que. push ( { nextx, nexty} ) ;
visited[ nextx] [ nexty] = true ;
}
}
}
}
四方格的地图
( 0 , 1 )
( - 1 , 0 ) ( 0 , 0 ) ( 1 , 0 )
( 0 , - 1 )
code python
int dir = [ [ 0 , 1 ] , [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , - 1 ] ] ; // 表示四个方向
from collections import deque
def bfs ( grid, visited, x, y) {
que = deque( )
que. append( [ x, y] )
visited[ x] [ y] = True
while que:
cur = que. popleft( )
curx, cury = cur[ 0 ] , cur[ 1 ]
for i in range ( 4 ) :
nextx = curx + dir [ i] [ 0 ]
nexty = cury + dir [ i] [ 1 ]
if nextx < 0 or nextx >= len ( grid) or nexty < 0 or nexty >= len ( grid[ 0 ] ) : continue
if not visited[ nextx] [ nexty] :
que. append( [ nextx] [ nexty] )
visitied[ ] [ ] = True
总结
当然广搜还有很多细节需要注意的地方,后面我会针对广搜的题目还做针对性的讲解。
因为在理论篇讲太多细节,可能会让刚学广搜的录友们越看越懵,所以细节方面针对具体题目在做讲解。
本篇我们重点讲解了广搜的使用场景,广搜的过程以及广搜的代码框架。
其实在二叉树章节的层序遍历中,我们也讲过一次广搜,相当于是广搜在二叉树这种数据结构上的应用。
这次则从图论的角度上再详细讲解一次广度优先遍历。