图
图的基本结构
G=(V,E),其中 V 代表 G 中结点的有限非空集合,E 为 G 中边的有限集合
有序(有向图)
用 <u,v> 表示一条有向边
注:u 为始点(尾),v 为终点(头)
flowchart LR
u-->v
无序(无向图)
(u,v)和(v,u)是同一条边
自回路
存在无向边(u,u)或有向边 <u,u>
多重图
两个顶点间允许有多条相同边
完全图
有最多的边数
- n个顶点
- 无向完全图:n(n-1)/2 条边
- 有向完全图:n(n-1) 条边
邻接:有边连接的两个顶点
关联:边和顶点连接的关系
连通图
连通:两点间存在路径
连通图:无向图中任意两个顶点间连通,如下图
连通分量:无向图的极大连通子图
如下图,为非连通图
- 连通分量可能有多个,不能只写最大的
- 每个连通分量必须是极大的
- 每个连通分量必须包含其顶点之间的所有边(见下)
强连通图:存在 u 到 v 路径的同时,还存在 v 到 u的路径
强连通分量:有向图的极大强连通子图
度
顶点的度:与该顶点相关联的边的数量
入度:顶点的入度即以该点为头(箭头)的边的数目,出度则为尾
生成树
无向连通图的生成树是一个极小连通子图
- 包含所有顶点
- 只有足以构成一棵树的边(n-1),加上一条边将构成回路
网
在图的边上加上数字作为权值(代价)
带权有向图的抽象数据类型
1 |
|
图的存储结构
因过于复杂,没有顺序结构
邻接矩阵表示法
邻接矩阵:表示顶点间相邻关系的矩阵
$$
A[u][v] = \begin{cases} 1, & \text{若 } (u,v) \text{ 或 } \langle u,v \rangle \in E \ 0, & \text{否则} \end{cases}
$$
若为网,则可定义为
$$
A[u][v] = \begin{cases} w, & \text{若 } (u,v) \text{ 或 } \langle u,v \rangle \in E \ 0, & \text{若 u=v}
\ \infty, & \text{否则}
\end{cases}
$$
其中,w 表示边上权值,无穷大表示计算机允许的、大于所有边上权值的数
优点
- 便于判断两个顶点之间是否有边(A[u][v]值)
- 便于计算各个顶点的度
- 无向图:邻接矩阵第 i 行元素和即为顶点 i 的度
- 有向图:第 i 行元素和即顶点 i 出度;第 i 列元素和即顶点 i 入度
缺点
- 不便于统计边的数目
- 时间复杂度为 O(n2)
- 空间复杂度高
- 空间复杂度为 O(n2)
邻接表表示法
下图为不带权的邻接表示例(未列出保存顶点信息的数据域——应在头尾中间):
下图分别为带权和不带权的图示(简化条件下的表示可以将头链表转为使用数组代替)
优点
- 便于统计边的数目
- 时间复杂度 O(n+e),其中 n 为顶点,e 为边
- 空间效率高
- 有向图/无向图:O(n+e)
- 适合稀疏图,稠密图建议邻接矩阵表示法
缺点
- 不便于判断顶点之间是否有边
- 最坏情况:O(n) 的时间
- 不便于计算有向图各个顶点的度
- 出度:第 u 个边链表中边结点个数
- 入读:遍历所有顶点的边链表
图的遍历
图遍历与树遍历的区别
- 可能存在到达不了的顶点
- 可能多次经过同一个顶点(回路)
解决办法:
- 辅助数组(判断是否被访问过)
- 防止多次访问
- 搜索结束后如果存在未标记顶点,应从图中另选一个未标记顶点出发,再次搜索
深度优先搜索遍历(DFS)
类似于树的先序遍历,递归
步骤
- 初始化标记数组
- 逐个检查顶点,使用 DFS 遍历,直到顶点全部访问完
DFS 操作方法
- 前进
- 从顶点 v 出发,访问任一未被访问的邻接顶点 w1
- 再从 w1 出发,重复上述步骤,直到全部访问完
- 回退
- 回退一步,若存在未被访问邻接顶点,则开始前进,否则继续回退直到遍历完成
注:如果中间出现路径不连通而导致的重新搜索,会得到 深度优先搜索生成森林
代码示例
1 |
|
1 |
|
分析
- DFS 对有向图每条边只查看一次,对无向图要 2 次(无向图的边在逻辑上被视为双向)
- 时间复杂度
- 邻接表存储:O(n+e),其中 n 为顶点,e 为边
- 邻接矩阵存储:O(n2)
使用 DFS 查找割点和桥
1 |
|
在上述代码中,只需要画出图像,输入结点,就能很轻松的知道如何将图变为非连通
宽度优先搜索遍历
类似于树的层次遍历,非递归,也没有 DFS 的回退情况
步骤
- 初始化标记数组
- 检查每个顶点,若未被访问,则使用 BFS 遍历
BFS 操作方法
- 从某个顶点出发
- 依次访问未访问过的邻接点,直到都访问完
- 可以使用队列来保存已访问过但其邻接点尚未考察的顶点
代码示例
1 |
|
1 |
|
分析
- 每个顶点进出队列各一次
- 每个出队顶点,检查所有邻接点,无向图每条边查两次
- 时间复杂度
- 邻接表存储:O(n+e),其中 n 为顶点,e 为边
- 邻接矩阵存储:O(n2)
拓扑排序
顶点活动网络(AOV 网)
顶点代表活动,有向边表示活动之间领先关系的有向图
注:AOV 网不应有环,应该是一个有向无环图(DAG 图)
判断 AOV 网是否有环:对有向图顶点进行拓扑排序,若所有顶点都在拓扑序列中,则该 AOV 网必定不存在环
拓扑排序与拓扑序列
拓扑排序
将 AOV 网中所有顶点线性排列
特点:若顶点 a 到 b 有一条路径,则序列中 a 必在 b 之前
拓扑序列
AOV 网中顶点的线性序列
若在网中,a 领先于 b,则在线性序列中 a 是 b 的前驱结点
算法及其实现
拓扑排序过程
- 找一个入度为 0 的顶点输出
- 删除该顶点及其所有出边
- 重复,直到所有顶点都输出(或无入度为 0 的顶点)
基本操作
- 计算每个顶点的入度
- 可以计算每个顶点的直接前驱的个数
- 删除该顶点的所有出边
- 将该顶点的邻接点入度减一
- 计算各顶点入度存于数组,将入度为 0 的顶点进栈
- 若栈不空,重复
- 顶点 a 出栈并保存在拓扑序列数组中
- 将 a 所有邻接点入度减一,若出现减后入度为 0,进栈
- 若还有顶点未输出,表明有环,无法拓扑排序,否则成功
分析
- n 顶点,e 边
- 计算入度并存于数组:O(e)
- 初始化堆栈 & 进栈:O(n)
- 排序过程(有向无环):顶点各进出一次,入度减一循环 e 次,总计 O(n+e)
关键路径
AOE 网
带权的有向无环图,边权表示活动持续时间。可以用来估算一项工程的完成时间
特点
- 无环情况下,只有一个入度为 0 的点(源点),表示工程开始
- (无环)只有一个出度为 0 的点(汇点),表示工程结束
关键路径
从开始顶点到完成顶点的最长路径
最短时间:关键路径的长度,关键路径上边代表的活动称为关键活动
事件相关
事件 a 可能的最早发生时间 Eearly(vi):开始顶点 v0 到 顶点 a 的最长路径长度
事件 a 允许的最迟发生时间 Elate(vi):不影响工期的条件下,a 允许的最晚发生时间
活动相关
活动 a 可能的最早开始时间 Aearly(a) = Eearly(vi)
活动 a 允许的最迟发生时间 Alate(a) = Elate(vj)-w(vi-vj)(权值):不影响工期的条件下,a 允许的最晚发生时间
求解关键路径
求解过程~O(n+e)
- 对顶点拓扑排序
- 按拓扑序列求每个事件可能的 Eearly(vi)
- 按逆拓扑序列求每个事件允许的 Elate(vi)
- 求出每个活动的 Aearly(a) 和 Alate(a)
- 关键活动:Aearly(a) = Alate(a)
把它们连起来就是关键路径
最小代价生成树
生成树的代价:各边上的代价之和
最小代价生成树:带权图的各生成树中,具有最小代价的生成树
普里姆算法
过程
- 初始状态:构造生成树 T 只有一个任意选定的起始项点,无边
- 重复执行下列运算:
- 从 T 的所有入边中找一条代价最小的边 e
- 将边 e 及其不属于 T 的那个顶点加入 T,直到所有顶点均加入 T 为止
最小代价生成树 T(包含 n-1 条边)
注:选择代价最小的入边时,如果存在多条同样权值的边可选,任选其一即可
数据结构
- 邻接表存储
- 3个一维数组
- 存放与顶点 i 距离最近且在生成树上的顶点
- 存放边的权值
- 标记顶点是否已经在生成树上
时间复杂度
n 个顶点,e 条边,O(n2)
克鲁斯卡尔算法
过程
- 初始状态:T 只包含 G 中所有顶点,无边
- 重复:
- 在 E 中选择一条代价最小的边 (u,v),并将其从 E 中删除
- 在生成树边集合中加入 (u,v),未形成回路,则加入,否则选下一条边
- 重复直到边集合包含 n-1 条边为止
最小代价生成树 T(包含 n-1 条边)
数据结构
- 邻接表存储
- 2个一维数组
- 用于存储图中所有边的信息
- 两个顶点信息和权值(排序算法选代价最小)
- 各顶点所属的连通分量(两个顶点属于不同连通分量,则加入它们关联的边后不会形成回路)
- 用于存储图中所有边的信息
时间复杂度
n 个顶点,e 条边,无向图
- 堆排序权值:O(e*log2e)
- 选择代价最小边加入生成树:O(e*log2e)
总体时间复杂度:O(e*log2e)
单源最短路径
最短路径:路径长度最短(路径上边的权值之和最小)
单源最短路径问题:(带权有向图 G 和源点 v0),求 v0 到 V 中其余各顶点的最短路径
Djikstra 算法
按路径长度的非递减次序逐一产生最短路径
求解过程
- 将 V 中顶点分成两组
- S:已求得最短路径的顶点集合(初始只包括源点)
- T=V-S:尚未确定最短路径的顶点集合(初始为V-{v0})
- 将 T 中顶点按最短路径非递减次序加入 S
- 计算 v0 到 T 中各顶点 vi 的当前最短路径,取最短为下一条路径,将终点加入 S,直到 S == V
数据结构
- 邻接表存储
- 3个一维数组
- 顶点 vi 是否在集合中
- 从源点 v0 到 vi 的当前最短路径长度
- vi 的前驱顶点
时间复杂度
n 个顶点,e 条边,有向图
O(n2):源点到某一特定顶点/单源最短路径
O(n3):任意两对顶点之间的最短路径