图论这一章尤其需要图例进行说明,方便理解,对于作者来说很费时间,本文主要为自己复习方便,所以并不会写的非常详细,见谅。
图论
图的基本概念
基本要素:
- 边
- 节点
两点连成线,多个点连成的线称为图。当然也可以就一个节点,或者啥也没有(空图)。
图的种类
方向的概念
根据边有无方向划分为:
- 无向图
- 有向图
权重的概念
边可以有权重,根据有无权重和方向:
- 加权有向图
- 加权无向图
度的概念
- 针对无向图,对于某节点,有几条边连着该节点,就称该节点的度为多少
- 对于有向图,每个节点有出度和入度
- 出度:从该节点出发的边的个数
- 入度:进入该节点的边的个数
连通性的概念
无向图中,根据任意两个节点之间能否到达:
- 连通图:如果任意两个节点都是可以到达的,可以称之为连通图。
- 非连通图:反之,如果有任意两个节点不能到达,就是非连通图。
有向图中,注意,是任意两个节点都能互相到达,单向的到达不是强连通图!
- 强连通图:在有向图中,任何两个节点是可以相互到达的,我们称之为 强连通图。
连通分量的概念
在无向图中,极大连通子图称为该图的一个连通分量。这里最重要的概念是 “ 极大 ” ,只有极大的连通子图才是连通分量!
强连通分量
在有向图中,极大强连通图称为该图的强连通分量。
这里,既是“ 极大 ” , 又是“ 强(任意节点双向可到达) ”,才是强连通分量。
图的构造
基本概念搞清楚了,那么如何用代码表示图?
一般包括:邻接表、邻接矩阵或者类
- 邻接矩阵(Adjacency Matrix)
- 对于一个有 n n n 个顶点的图 G = ( V , E ) G=(V,E) G=(V,E),其邻接矩阵是一个 n × n n×n n×n 的矩阵 A A A。如果图是无向图,当顶点 i i i 和顶点 j j j 之间有边相连时, A i j = A j i = 1 A_{ij}=A_{ji}=1 Aij=Aji=1(若为加权图,则为边的权重值),否则为 0;
- 如果图是有向图,当存在从顶点 i i i 到顶点 j j j 的边时, A i j = 1 A_{ij}=1 Aij=1(或边的权重), A j i A_{ji} Aji 则根据是否有从 j j j 到 i i i 的边来确定。
- 这种表示方法的优点是判断两个顶点是否相邻很方便,时间复杂度为 O ( 1 ) O(1) O(1)。缺点是对于稀疏图,会浪费大量的存储空间,因为大量的元素为 0。
- 例如,一个有 1000 个顶点但只有 100 条边的图,邻接矩阵需要存储 1000 × 1000 = 1000000 1000×1000 = 1000000 1000×1000=1000000 个元素,但实际有用的信息很少。
上面的描述有点抽象
- 简单说,邻接矩阵就是一个二维矩阵(计算机中叫二维数组)来表示图结构,以节点为主体来描述图
- 有多少节点就申请多大的二维数组
- 例如: g r i d [ 2 ] [ 5 ] = 6 grid[2][5] = 6 grid[2][5]=6,表示 节点 2 节点 2 节点2 连接 节点 5 节点5 节点5 为有向图,节点2 指向 节点5,边的权值为 6 6 6。
- 如何用邻接矩阵表示无向图呢? g r i d [ 2 ] [ 5 ] = 6 grid[2][5] = 6 grid[2][5]=6, g r i d [ 5 ] [ 2 ] = 6 grid[5][2] = 6 grid[5][2]=6,表示节点2 与 节点5 相互连通,权值为6。
分析邻接矩阵的优缺点:
缺点:
- 一个有 N N N 个节点的图,需要申请 N 2 N^2 N2 尺寸的二维数组,需要 N 2 N^2 N2 尺寸的空间,显然邻接矩阵这种表达方式容易造成空间浪费,
- 并且,在寻找节点连接情况的时候,需要遍历整个矩阵,即时间复杂度也相当高,达到了 O ( l o g N 2 ) O (logN^2) O(logN2)
优点:
- 表达方式简答,易于理解
- 检查任意两个顶点之间是否存在边的操作非常快
- 适合稠密图,在边数接近顶点平方数的图中,邻接矩阵是一种空间效率较高的表示方法
- 邻接表(Adjacency List)
- 对于图中的每个顶点,用一个链表(或其他合适的数据结构,如数组等)来存储与它相邻的顶点。
- 在有向图中,可以分别存储出边邻接表和入边邻接表。这种表示方法对于稀疏图很节省空间,只存储实际存在的边信息。
- 其优点是存储效率高,尤其是在边数较少的情况下。
- 缺点是查询两个顶点是否相邻的时间复杂度比邻接矩阵高,需要遍历链表,平均时间复杂度为 O ( d ) O(d) O(d)( d d d 为顶点的度)。
- 例如,在一个社交网络图中,如果用邻接表表示,每个用户的好友列表就是其邻接表,这样可以高效地存储和查询用户之间的关系。
简单说:
- 邻接表以边为主体,使用数组+链表来表示边的连接情况,有多少边就申请多少对应大小的链表
- 优点:
- 对于稀疏图的存储,只需要存储边,空间利用率高
- 遍历节点连接情况,相对容易
- 缺点:
- 检查任意两个节点之间是否存在边,效率相对较低,需要 O ( V ) O(V) O(V)时间,V表示某节点连接其他节点的数量
- 实现相对复杂,不直观,不易理解
图的遍历方式
- 深度优先搜索(DFS)
- 广度优先搜索(BFS)
注意!
- 二叉树的递归遍历,是dfs在二叉树上的遍历方式
- 二叉树的层序遍历,是bfs在二叉树上的遍历方式
-
深度优先搜索(DFS - Depth - First - Search)
- 从图中的某个起始顶点 v v v 开始,首先访问顶点 v v v,
- 然后选择一个与 v v v 相邻且未被访问过的顶点 w w w,递归地对 w w w 进行深度优先搜索,直到遇到一个顶点,其所有相邻顶点都已被访问过,然后回溯到上一个有未访问邻接顶点的顶点,继续搜索。
- 这个过程可以使用栈(递归调用本身就利用了系统栈)来实现。
- 例如,在一个迷宫图中,可以想象为沿着一条通道一直走,直到走不通了,再返回上一个岔路口选择另一条路。
- 在实现中,可以使用一个布尔数组来标记顶点是否已被访问。深度优先搜索可以用来判断图是否连通、寻找图的连通分量、生成图的生成树等。
-
广度优先搜索(BFS - Breadth - First - Search)
- 从起始顶点 v v v 开始,首先访问顶点 v v v,然后依次访问 v v v 的所有未被访问过的邻接顶点,接着再依次访问这些邻接顶点的未被访问过的邻接顶点,以此类推,按照层次进行遍历。这个过程可以使用队列来实现。例如,在一个网络拓扑图中,如果从一个节点开始传播信息,BFS 可以模拟信息按照距离逐步传播的过程。
- 广度优先搜索可以用于计算图中顶点的最短路径(在无权图中,距离就是边的数目)、判断图的连通性等。在实现时,同样需要一个布尔数组来标记顶点是否已被访问,还需要一个队列来存储待访问的顶点。
最短路径算法
-
迪杰斯特拉算法(Dijkstra’s Algorithm)
- 用于计算加权有向图(或加权无向图)中一个顶点到其他所有顶点的最短路径。算法维护一个集合 S S S,表示已经找到最短路径的顶点集合,初始时只包含起始顶点。对于每个顶点 v v v,记录从起始顶点到 v v v 的当前最短距离 d [ v ] d[v] d[v]。在每次迭代中,选择不在 S S S 中且 d [ v ] d[v] d[v] 最小的顶点 u u u,将其加入 S S S,然后更新与 u u u 相邻的顶点的最短距离估计。例如,在一个交通网络中,要找到从一个城市到其他所有城市的最短距离,迪杰斯特拉算法可以通过逐步扩展已确定最短路径的城市集合来计算。
- 算法的时间复杂度一般为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)( ∣ V ∣ |V| ∣V∣ 是顶点数),使用合适的数据结构(如优先队列)可以优化到 O ( ∣ E ∣ + ∣ V ∣ l o g ∣ V ∣ ) O(|E| + |V|log|V|) O(∣E∣+∣V∣log∣V∣)( ∣ E ∣ |E| ∣E∣ 是边数)。
-
贝尔曼 - 福特算法(Bellman - Ford Algorithm)
- 可以处理有负权重边的图(但不能有负权重环),用于计算单源最短路径。算法通过对所有边进行多次松弛操作来逐步逼近最短路径。每次迭代,遍历图中的所有边,尝试更新通过该边连接的两个顶点的最短距离。例如,在一些经济模型中,可能会出现负权重的情况,贝尔曼 - 福特算法可以用于分析成本等相关问题。
- 算法的时间复杂度为 O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(∣V∣∣E∣)。
-
弗洛伊德算法(Floyd - Warshall Algorithm)
- 用于计算加权图(有向或无向)中所有顶点对之间的最短路径。算法使用动态规划的思想,通过一个二维数组来存储顶点之间的最短距离,逐步更新数组的值。例如,在分析一个大型通信网络中任意两个节点之间的最短通信路径时,弗洛伊德算法可以一次性计算出所有的最短路径信息。
- 算法的时间复杂度为 O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)。
生成树
-
生成树的定义
- 对于一个连通图 G = ( V , E ) G=(V,E) G=(V,E),生成树是 G G G 的一个子图 T = ( V , E ′ ) T=(V,E') T=(V,E′),其中 E ′ ⊆ E E'\subseteq E E′⊆E,且 T T T 是一棵树(即无环且连通),包含图 G G G 的所有顶点。例如,在一个电网图中,生成树可以表示一种最小的电线连接方式,使得所有节点都能有电且没有多余的回路。
-
最小生成树(MST - Minimum Spanning Tree)
- 在加权连通图中,所有生成树中边的权重之和最小的生成树称为最小生成树。常见的计算最小生成树的算法有:
- 普里姆算法(Prim’s Algorithm):从任意一个顶点开始,每次选择一个与当前生成树相连且边权重最小的顶点,将其加入生成树,直到所有顶点都被包含。算法时间复杂度一般为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),使用合适的数据结构可以优化到 O ( ∣ E ∣ + ∣ V ∣ l o g ∣ V ∣ ) O(|E| + |V|log|V|) O(∣E∣+∣V∣log∣V∣)。
- 克鲁斯卡尔算法(Kruskal’s Algorithm):将图中的所有边按照权重从小到大排序,然后依次选择边,如果选择的边不会形成环,则将其加入生成树,直到生成树包含 n − 1 n - 1 n−1 条边( n n n 是顶点数)。算法时间复杂度主要取决于边的排序,一般为 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(∣E∣log∣E∣)。最小生成树在网络设计、电路布线等领域有广泛应用。
拓扑排序
-
拓扑排序的定义
- 对于一个有向无环图(DAG - Directed Acyclic Graph),拓扑排序是将图中所有顶点排成一个线性序列,使得对于图中的任意一条有向边 ( u , v ) (u,v) (u,v),顶点 u u u 在序列中都排在顶点 v v v 的前面。例如,在一个课程学习的先后顺序图中,拓扑排序可以表示课程的学习顺序,先修课程排在前面,后修课程排在后面。
-
拓扑排序算法
- 一种常见的方法是使用深度优先搜索。在 DFS 遍历过程中,当一个顶点的所有后继顶点都被访问完后,将该顶点加入拓扑排序序列。也可以使用入度表的方法,不断删除入度为 0 的顶点,并更新其邻接顶点的入度,直到所有顶点都被处理。拓扑排序在任务调度、项目规划等领域有重要应用,用于确定任务的执行顺序。