图论理论基础
这篇我们将正式开始学习图论!
在代码随想录中,图论相关的算法题目将统一使用ACM模式。为什么要使用ACM模式呢?
图的基本概念
在二维坐标中,两点可以连成线,多个点连成的线就构成了图。
当然,图也可以仅有一个节点,甚至没有节点(即为空图)。
图的种类
整体上,图一般分为有向图和无向图。
- 有向图是指图中的边是有方向的。
- 无向图是指图中的边没有方向。

此外,还有加权有向图和加权无向图,即图中边是有权值的。

度
- 在无向图中,一个节点的度是指有几条边连接该节点。

例如,在下面的无向图中,节点4的度为5,节点6的度为3。
- 在有向图中,每个节点有出度和入度。
- 出度:从该节点出发的边的个数。
- 入度:指向该节点的边的个数。
例如,在下面的有向图中,节点3的入度为2,出度为1;节点1的入度为0,出度为2。

连通性
在图中表示节点的连通情况,我们称之为连通性。
连通图
在无向图中,如果任何两个节点都是可以到达的,我们称之为连通图。

如果有节点不能到达其他节点,则为非连通图。

强连通图
在有向图中,如果任何两个节点是可以相互到达的,我们称之为强连通图。

连通分量
在无向图中,极大连通子图称之为该图的一个连通分量。
例如,在下面的无向图中,节点1、节点2、节点5构成的子图是该无向图的一个连通分量。
同理,节点3、节点4、节点6构成的子图也是该无向图的一个连通分量。

强连通分量
在有向图中,极大强连通子图称之为该图的强连通分量。

图的构造
我们如何用代码来表示一个图呢?一般使用邻接表、邻接矩阵或者用类来表示。
邻接矩阵
邻接矩阵使用二维数组来表示图结构。其优点是表达方式简单,易于理解,检查任意两个顶点间是否存在边的操作非常快,适合稠密图。缺点是遇到稀疏图,会导致申请过大的二维数组造成空间浪费,且遍历边的时候需要遍历整个n*n矩阵,造成时间浪费。

邻接表
邻接表使用数组+链表的方式来表示。其优点是对于稀疏图的存储,只需要存储边,空间利用率高,遍历节点连接情况相对容易。缺点是检查任意两个节点间是否存在边,效率相对低,需要O(V)时间,V表示某节点连接其他节点的数量,且实现相对复杂,不易理解。

图的遍历方式
图的遍历方式基本是两大类:
- 深度优先搜索(dfs)
- 广度优先搜索(bfs)
在讲解二叉树章节的时候,其实就已经讲过这两种遍历方式。二叉树的递归遍历是dfs在二叉树上的遍历方式,而二叉树的层序遍历是bfs在二叉树上的遍历方式。
dfs和bfs是一种搜索算法,可以在不同的数据结构上进行搜索。在二叉树章节里是在二叉树这样的数据结构上搜索。而在图论章节,则是在图(邻接表或邻接矩阵)上进行搜索。
深度优先搜索理论基础
dfs 与 bfs 区别
提到深度优先搜索(dfs),就不得不说和广度优先搜索(bfs)有什么区别。
先来了解dfs的过程,很多录友可能对dfs(深度优先搜索),bfs(广度优先搜索)分不清。
先给大家说一下两者大概的区别:
- dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
- bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。
当然以上讲的是,大体可以这么理解,接下来我们详细讲解dfs,(bfs在用单独一篇文章详细讲解)
dfs 搜索过程
上面说道dfs是可一个方向搜,不到黄河不回头。那么我们来举一个例子。
如图一,是一个无向图,我们要搜索从节点1到节点6的所有路径。

那么dfs搜索的第一条路径是这样的:(假设第一次延默认方向,就找到了节点6),
此时我们找到了节点6,(遇到黄河了,是不是应该回头了),那么应该再去搜索其他方向了。如图三:

路径2撤销了,改变了方向,走路径3(红色线),接着也找到终点6。那么撤销路径2,改为路径3,在dfs中其实就是回溯的过程(这一点很重要,很多录友不理解dfs代码中回溯是用来干什么的)
又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图四中,路径4撤销(回溯的过程),改为路径5。

又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图五,路径6撤销(回溯的过程),改为路径7,路径8和路径7,路径9,结果发现死路一条,都走到了自己走过的节点。

那么节点2所连接路径和节点3所链接的路径都走过了,撤销路径只能向上回退,去选择撤销当初节点4的选择,也就是撤销路径5,改为路径10。如图图六:

上图演示中,其实我并没有把所有的从节点1到节点6的dfs(深度优先搜索)的过程都画出来,那样太冗余了,但已经把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
图论理论基础及搜索算法解析

最低0.47元/天 解锁文章
1143

被折叠的 条评论
为什么被折叠?



