图可以由顶点集和边集组成
V:(Vertex) 顶点集,eg:{A, B, C, D}
E、Arc:(Edge、Arc弧) 边集,eg:{(A, B), (B, C), (C, D), (D, A)}
E 是有向边的集合,一条边是两个顶点的有序对,也称为 弧。
记 顶点 v 到顶点 w 的弧 为
E 是无向边的集合,一条边是两个顶点的无序对,也称为 弧。
记 顶点 v 到顶点 w 的弧 为 (v, w),注意用圆括号表示有向边,也称 顶点 w 和顶点 v 互为邻接点,边 (v, w) 依附于顶点 w 和 v,或边 (v, w) 和顶点 v、w 相关联。
只研究简单图
无向图:
度——依附于顶点边的条数,记为 TD(v)
∑ i = 1 n T D ( v i ) = 2 e \begin{array}{l} \sum\limits_{i = 1}^n {TD({v_i})} = 2e \end{array} i=1∑nTD(vi)=2e
有向图:
入度——以顶点为终点的边,记为 ID(v)——In-degree
出度——以顶点为起点的边,记为 OD(v)——Out-degree
对 于 一 个 顶 点 而 言 , T D ( v ) = I D ( v ) + O D ( v ) 对 于 有 n 个 顶 点 、 e 条 边 的 有 向 图 , ∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = e 全 部 顶 点 的 入 度 之 和 与 出 度 之 和 相 等 , 并 且 等 于 边 数 。 \begin{array}{l} 对于一个顶点而言,TD(v) = ID(v) + OD(v) \\ 对于有\ n\ 个顶点、e\ 条边的有向图,\sum\limits_{i = 1}^n {ID({v_i})} = \sum\limits_{i = 1}^n {OD({v_i})} = e \\ 全部顶点的入度之和与出度之和相等,并且等于边数。 \end{array} 对于一个顶点而言,TD(v)=ID(v)+OD(v)对于有 n 个顶点、e 条边的有向图,i=1∑nID(vi)=i=1∑nOD(vi)=e全部顶点的入度之和与出度之和相等,并且等于边数。
路径――顶点v,到顶点v之间的一条路径是指顶点序列,VpVi,v, ,.0,vim , vq。
路径长度――路径上边的数目。
回路――第一个顶点和最后一个顶点相同的路径称为回路或环。
简单路径――在路径序列中,顶点不重复出现的路径称为简单路径。
简单回路――除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
点到点的距离――从顶点 u 出发到顶点 v 的最短路径若存在,则此路径的长度称为从 u 到 v 的距离。若从 u 到 v 根本不存在路径,则记该距离为无穷*(∞)* 。
无向图中,若从顶点 v 到顶点 w 有路径存在,则称 v 和 w 是连通的。
有向图中,若从顶点 v 到顶点 w 和从顶点 w 到顶点 v 之间都有路径,则称这两个顶点是强连通的
无向图
有向图
生成树
包含图中全部顶点的一个极小连通子图
生成森林
非连通图中,连通分量的生成树构成生成森林。
每条边有权值
带权路径长度:当图是权图时,一条路径上所有边的权值之和,称为该路径的带权路径和长度。
有 C n 2 = n ( n − 1 ) 2 条 边 的 无 向 图 称 为 完 全 图 有 A n 2 = n ( n − 1 ) 条 边 的 有 向 图 称 为 有 向 完 全 图 边 数 满 足 E ∣ < ∣ V ∣ log ∣ V ∣ 的 图 称 为 稀 疏 图 反 之 则 为 稠 密 图 \begin{array}{l} 有\ C_n^2 = \frac{{n(n - 1)}}{2}\ 条边的无向图称为完全图 \\ 有\ A_n^2 = n(n - 1)\ 条边的有向图称为有向完全图 \\ 边数满足\ E| < |V|\log |V|\ 的图称为稀疏图 \\ 反之则为稠密图 \end{array} 有 Cn2=2n(n−1) 条边的无向图称为完全图有 An2=n(n−1) 条边的有向图称为有向完全图边数满足 E∣<∣V∣log∣V∣ 的图称为稀疏图反之则为稠密图
一个顶点的入度为 0 、其余顶点的入度均为 1 的有向图,称为有向树。
有 向 图 和 无 向 图 均 有 : ∑ i = 1 n T D ( v i ) = 2 e 有 向 图 : 对 于 一 个 顶 点 而 言 , T D ( v ) = I D ( v ) + O D ( v ) 对 于 有 n 个 顶 点 、 e 条 边 的 有 向 图 , ∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = e 全 部 顶 点 的 入 度 之 和 与 出 度 之 和 相 等 , 并 且 等 于 边 数 。 连 通 性 : 对 于 n 个 顶 点 的 无 向 图 G , 若 G 是 连 通 图 , 则 最 少 有 n − 1 条 边 若 G 是 非 连 通 图 , 则 最 多 可 能 有 C n − 1 2 条 边 ( 令 n − 1 条 边 两 两 连 接 ) 对 于 n 个 顶 点 的 有 向 图 G , 若 G 是 强 连 通 图 , 则 最 少 有 n 条 边 ( 令 其 形 成 回 路 ) 完 全 图 : 有 C n 2 = n ( n − 1 ) 2 条 边 的 无 向 图 称 为 完 全 图 有 A n 2 = n ( n − 1 ) 条 边 的 有 向 图 称 为 有 向 完 全 图 \begin{array}{l} 有向图和无向图均有:\ \sum\limits_{i = 1}^n {TD({v_i})} = 2e \\ \\ 有向图:\\对于一个顶点而言,TD(v) = ID(v) + OD(v) \\ 对于有\ n\ 个顶点、e\ 条边的有向图,\sum\limits_{i = 1}^n {ID({v_i})} = \sum\limits_{i = 1}^n {OD({v_i})} = e \\ 全部顶点的入度之和与出度之和相等,并且等于边数。\\ \\ 连通性:\\对于\ n\ 个顶点的无向图\ G,若\ G\ 是连通图,则最少有\ n-1\ 条边 \\ \qquad \qquad \qquad \qquad \qquad \quad若 G 是非连通图,则最多可能有 C_{n - 1}^2 条边(令\ n-1\ 条边两两连接)\\ 对于\ n\ 个顶点的有向图\ G,若\ G\ 是强连通图,则最少有\ n\ 条边(令其形成回路)\\ \\ 完全图:\\ 有\ C_n^2 = \frac{{n(n - 1)}}{2}\ 条边的无向图称为完全图 \\ 有\ A_n^2 = n(n - 1)\ 条边的有向图称为有向完全图 \\ \end{array} 有向图和无向图均有: i=1∑nTD(vi)=2e有向图:对于一个顶点而言,TD(v)=ID(v)+OD(v)对于有 n 个顶点、e 条边的有向图,i=1∑nID(vi)=i=1∑nOD(vi)=e全部顶点的入度之和与出度之和相等,并且等于边数。连通性:对于 n 个顶点的无向图 G,若 G 是连通图,则最少有 n−1 条边若G是非连通图,则最多可能有Cn−12条边(令 n−1 条边两两连接)对于 n 个顶点的有向图 G,若 G 是强连通图,则最少有 n 条边(令其形成回路)完全图:有 Cn2=2n(n−1) 条边的无向图称为完全图有 An2=n(n−1) 条边的有向图称为有向完全图
#define MaxVertexNum 100
typedef struct {
VertexType Vex[MaxVertexNum]; // 顶点表
bool mark[MaxVertexNum]; // 标志顶点表该位置是否有顶点,若为 false,则可能被删除了,或原本没数据
EdgeType Edge[MaxVertexNum][MaxVertexNum]; // 邻接矩阵,边表,表示是否连通
int vexnum, arcnum; // 图的当前顶点和弧数
}MGraph;
VertexType
可以是字符型,结构体型等等
EdgeType
可以是 int 型,也可以是 bool 型,后者仅占用 1B,节省空间。
对于无向图来说,邻接矩阵必为对称矩阵,因此可以压缩存储。
对于有向图来说,Edge[i][j]
表示 从 i 到 j 是否存在边
第 i 行表示,从 i 结点出发的弧
第 j 列表示,到达 j 结点的弧
#define MaxVertexNum 100
#define INFINITY 0x7FFFFFFF // int 最大值
typedef struct {
VertexType Vex[MaxVertexNum]; // 顶点表
bool mark[MaxVertexNum]; // 标志顶点表该位置是否有顶点,若为 false,则可能被删除了,或原本没数据
EdgeType Edge[MaxVertexNum][MaxVertexNum]; // 边的权,权表,不仅表示连通与否,还附带了权值
int vexnum, arcnum; // 图的当前顶点和弧数
}MGraph;
Edge[i][j]
中 i = j 时,表示指向顶点自身的边,此时权值为 0,或 ∞ ,如果边不存在,则权值为 ∞ 。
无 向 图 : 1. 顶 点 i 的 度 = T D ( v i ) = 第 i 行 ( 第 i 列 ) 非 零 元 素 个 数 有 向 图 : 1. 顶 点 i 的 入 度 = I D ( v i ) = 第 i 列 非 零 元 素 个 数 2. 顶 点 i 的 出 度 = O D ( v i ) = 第 i 行 非 零 元 素 个 数 时 间 复 杂 度 和 空 间 复 杂 度 都 较 高 设 图 G 的 邻 接 矩 阵 为 A , A 进 行 矩 阵 乘 法 运 算 , A 的 n 次 幂 A n 记 为 矩 阵 B , 则 B 中 的 元 素 B [ i ] [ j ] 表 示 由 顶 点 i 到 顶 点 j 的 路 径 长 度 为 n 的 路 径 的 数 目 。 \begin{array}{l} 无向图:\\ 1.\ 顶点\ i\ 的度={TD({v_i})}=第\ i\ 行(第\ i\ 列)非零元素个数\\ \\ 有向图:\\ 1.\ 顶点\ i\ 的入度={ID({v_i})}=第\ i\ 列非零元素个数 \\ 2.\ 顶点\ i\ 的出度={OD({v_i})}=第\ i\ 行非零元素个数 \\ \\ 时间复杂度和空间复杂度都较高\\ \\ 设图\ G\ 的邻接矩阵为\ A\ ,A\ 进行矩阵乘法运算,A\ 的\ n\ 次幂\ {A^n}\ 记为矩阵\ B,\\ 则\ B中的元素\ B[i][j]\ 表示由顶点\ i\ 到顶点\ j\ 的路径长度为\ n\ 的路径的数目。 \end{array} 无向图:1. 顶点 i 的度=TD(vi)=第 i 行(第 i 列)非零元素个数有向图:1. 顶点 i 的入度=ID(vi)=第 i 列非零元素个数2. 顶点 i 的出度=OD(vi)=第 i 行非零元素个数时间复杂度和空间复杂度都较高设图 G 的邻接矩阵为 A ,A 进行矩阵乘法运算,A 的 n 次幂 An 记为矩阵 B,则 B中的元素 B[i][j] 表示由顶点 i 到顶点 j 的路径长度为 n 的路径的数目。
类似树的孩子表示法,但是要存储边集和点集
typedef struct ArcNode { // 边表结点
int adjvex; // 该弧所指向的顶点的位置
struct ArcNode *next; // 指向下一条弧的指针
InfoType info; // 网的边权值
}ArcNode;
typedef struct VNode {
VertexType data; // 顶点信息
ArcNode *first; // 指向第一条依附该顶点的弧的指针
}VNode, AdjList[MaxVertexNum];
typedef struct ALGraph {
AdjList vertices; // 邻接表
int vexnum, arcnum; // 图的顶点数和弧数
}ALGraph; // ALGraph 是以邻接表存储的图表类型
VNode 存储数据和 first 指针,这个 first 指针指向依附它的第一条弧,在弧的另一端,有它连接的顶点,所以该弧 ArcNode 可以存储 连接顶点在数组中的指针、该顶点的权值,从而获取到顶点的数据。
无向图 由于弧的关系是双向的,边结点的数目为 2 |E|,因此无向图的空间复杂度为 O(|V| + 2 |E|)。
有向图的空间复杂度为 O(|V| + |E|)。
存储有向图时,计算度、入度需遍历全部边结点,比较繁琐。
记住下面这个图,比较抽象,仔细体悟吧!
针对邻接表存储有向图时,计算度、入度比较繁琐,采用十字链表存储有向图。
// 边集,也是弧集, tail ———→ head
struct ArcNode {
int tailvex; // 存储弧的尾部的顶点 在图中的位置 顺序存储中的下标
int headvex; // 存储弧的头部的顶点 在图中的位置 顺序存储中的下标
ArcNode *hlink; // 存储与该弧 弧头相同的下一条弧(均指向 head 的下一条弧)
ArcNode *tlink; // 存储与该弧 弧尾相同的下一条弧(均从 tail 出发的下一条弧)
InfoType info; // 存储权的信息
};
// 顶点集
typedef struct VexNode {
ElemType data; // 顶点的数据内容
ArcNode *firstIn; // 将顶点视作 head,指针指向 指向该顶点的第一条弧
ArcNode *firstOut; // 将顶点视作 tail,指针指向 从该顶点出发的第一条弧
}VexList[MaxVertexSize];
// 图
struct ShiZiGraph {
VexList vertexs; // 顺序存储顶点
int vexNum, arcNum; // 点数和边数
}
计算出度:
将该顶点视为 tail,即从 VexNode
的 firstOut
开始遍历,每次寻找其 tlink
即可
计算入度:
将该顶点视为 head,即从 VexNode
的 firstIn
开始遍历,每次寻找其 hlink
即可
空间复杂度:O(|V| + |E|)
针对 无向图,若使用邻接表,则进行删除等操作时,由于信息冗余——边的信息有两份,需要删除两个顶点的边信息,比较繁琐,故使用邻接多重表。
// 边集,也是弧集,(i, j) i ———— j
struct ArcNode {
bool mark; // 标记是否被搜索过
int ivex; // 存储弧的 i 端顶点 在图中的位置 顺序存储中的下标
ArcNode *ilink; // 存储依附于该弧 i 端顶点的下一条弧
int jvex; // 存储弧的 j 端顶点 在图中的位置 顺序存储中的下标
ArcNode *jlink; // 存储依附于该弧 j 端顶点的下一条弧
InfoType info; // 存储权的信息
};
// 顶点集
typedef struct VexNode {
ElemType data; // 顶点的数据内容
ArcNode *firstEdge; // 指针指向 依附于该顶点的第一条弧
}VexList[MaxVertexSize];
// 图
struct DuoChongGraph {
VexList vertexs; // 顺序存储顶点
int vexNum, arcNum; // 点数和边数
}
计算度:
从顶点 VexNode
的 firstEdge
出发,找到一个弧时,计数,并判断其 ivex
和 jvex
,哪个存储的位置 = 该弧的位置,随后对应找其 link
,便可找到下一条弧。
删除操作:
删除弧:
不需要再维护两份信息,若要删除某个指定弧,该弧有 i 和 j 指示了弧两边的顶点,因此从这两个顶点出发找到该弧,进行删除操作即可。
删除顶点:
从该顶点出发,找到依附于该顶点的所有边结点,同时,因为这些边结点可能是其它顶点和该顶点的公共边,因此需要从其它顶点出发找到公共边——这些边结点,并令指向这些公共边的指针指向 NULL,之后删除顶点和边结点。
空间复杂度为:O(|V| + |E|)
Adjacent(G, x, y)
判断图 G 是否存在边
(x, y)
无向图:
***邻接矩阵:直接判断对应 Edge[x][y]
即可,时间复杂度为***O(1)
***邻接表:***需遍历 x 顶点的所有边结点,看是否有 y,时间复杂度最差为 ***O(|V| - 1)***。
有向图:
***邻接矩阵:直接判断对应 Edge[x][y]
即可,时间复杂度为***O(1)
***邻接表:***需遍历 x 顶点的所有边结点,看是否有 y,时间复杂度最优为***O(1)***,最差为 ***O(|V| - 1)***。
Neighbors(G, x)
列出图 G 中与结点 x 邻接的边无向图:
***邻接矩阵:直接判断对应 Edge
数组中,第 x 行或者第 x 列的元素即可,时间复杂度为***O(|V|)
***邻接表:***需遍历 x 顶点的所有边结点,看是否有 y,时间复杂度最优为***O(1)***,最差为 ***O(|V| - 1)***。
有向图:
***邻接矩阵:判断对应 Edge
的 x 行(出边)x 列(入边),时间复杂度为***O(|V|)
***邻接表:***需遍历 x 顶点的所有边结点,看是否有 y,时间复杂度最差为 ***O(|V| - 1)***。
InsertVertex(G, x)
在图 G 中插入顶点 x有向图和无向图:
***邻接矩阵:***Vex[MaxVertexNum]
中写入该顶点的信息即可,若使用 mark
数组标记是否存储了顶点信息,则需遍历第一个为 false 的,在此位置写入顶点信息,同时需要将 vexnum
加 1。时间复杂度为***O(1)***。
***邻接表:***在 vertices
数组中添加该顶点的信息即可,其 first
需指向 NULL。同时需要将 vexnum
加 1。时间复杂度为***O(1)***。
DeleteVertex(G, x)
在图 G 中删除顶点 x无向图:
***邻接矩阵:***Vex[MaxVertexNum]
中删除该顶点的信息,并在 mark
中对应置为 false,vexnum
减 1,Edge
数组对应的 x 行和 x 列均置为 0 。
***邻接表:***在 vertices
数组中需删除 x 结点和 x 结点链接的 边结点的空间,(若用 mark 数组,同上),同时需要在其他所有顶点的 边结点中找到 x ,并删除, vexnum
减 1,时间复杂度最优为***O(1)***,最差为 ***O(|E|)***。
有向图:
***邻接矩阵:***同上
***邻接表:***在 vertices
数组中需删除 x 结点和 x 结点链接的 边结点的空间(这一步删除了出边),同时在其他结点中找到 指向该顶点的边(这一步删除了入边), vexnum
减 1,删除出边时间复杂度为 ***O(1)~O(|V|)***,删除入边时间复杂度为 ***O(|E|)***。
AddEdge(G, x, y)
若无向边 (x, y) 或 有向边 无向图:
***邻接矩阵:***注意vexnum
+ 1
***邻接表:***遍历 x 结点,使用头插法(尾插法复杂度高)
有向图:
***邻接矩阵:***同上
***邻接表:***同上
RemoveEdge(G, x, y)
若无向边 (x, y) 或 有向边 无向图:
***邻接矩阵:***找到 Edge[x][y]
,置 0
***邻接表:***遍历 x 结点,找到 y 就删除,同时遍历 y,找到 x 删除。若没找到,则后续也不需遍历 y。
有向图:
***邻接矩阵:***找到 Edge[x][y]
,置 0
***邻接表:***遍历 x 结点,找到 y 就删除
FirstNeighbor(G, x)
求图中顶点 x 的第一个邻接点,若有则返回顶点号,若无则返回 -1。无向图:
***邻接矩阵:***遍历第 x 行,找到第一个即可。时间复杂度为 O(1)~O(|V|)
***邻接表:***遍历 x 结点,第一个就是了。
有向图:
***邻接矩阵:***出边扫描行,入边扫描列
***邻接表:***出边第一个顶点,入边遍历其他顶点。
NextNeighbor(G, x, y)
假设图 G 中顶点 y 是x 的一个邻接点,返回 y 外顶点 x 的下一个邻接点的顶点号,若 y 是最后 x 的最后一个邻接点,返回 -1。无向图:
***邻接矩阵:***第 x 行从第 y + 1 列开始找,找到第一个即可。时间复杂度为 O(1)~O(|V|)
***邻接表:***遍历 x 结点,从 y 后面一个就是了。时间复杂度为 O(1)
有向图:
***邻接矩阵:***同上
***邻接表:***同上
Get_edge_value(G, x, y)
获取图 G 中边 (x, y) 或 Adjacent(G, x, y)
用一用嘛
Set_edge_value(G, x, y, v)
设置图 G 中边 (x, y) 或 Adjacent(G, x, y)
用一用嘛
与树的广度优先遍历类似~依赖辅助队列完成。
树每次找的是左右孩子,不存在重复访问可能。
图找该顶点的所有邻居顶点,但存在重复访问的可能性。
注意,有向图中,一个结点的邻居顶点是有方向的,在有向图中,有时候需要多次执行 BFS 才能遍历结束。
树,结点出队后,依靠 rchild 和 lchild 指针来找入队元素。
图,顶点出队后,将该顶点标志为 “遍历过”,需要 FirstNeighbor(G, x)
和 NextNeighbor(G, x, y)
两个函数找到该顶点的所有邻居顶点,找到邻居后,需要先判断是否遍历过,若没被遍历过,则入队它们。
这个代码我都写吐了
bool visited[Max_vex_num];
void BFSTraverse(Graph G) {
for (int i = 0; i < G.vexnum; i++)
visited[i] = false; // 标记数组初始化为 false
for (int i = 0; i < G.vexnum; i++)
if (visited[i] == false) // 找到未遍历的连通分量,并遍历它
BFS(G, i);
}
// 注意,这个 BFS 是遍历一个连通分量的函数,如果图中有多个彼此不连通的连通分量,则需要对多个连通分量调用 BFS 函数。
void BFS(Graph G, int v) { // 从顶点 v 开始,v 是下标广度优先遍历
LinkQueue Q; // 初始化一个队列
InitQueue(Q);
EnQueue(Q, v); // v 入队
while( ! isEmpyt(Q) ) { // 队列非空
DeQueue(Q, v); // 出队
visit(v); // 遍历 v
visited[v] = true; // v 置为遍历过
/* np >= 0 是因为那两个函数查找失败则返回 -1 */
for (np = FirstNeighbor(v); np >= 0; np = NextNeighbor(G, v, np)) {
if ( ! visited[np] ) // np 未被遍历过
EnQueue(np);
}// for
}// while
Destory(Q); // 本次执行完,就销毁该队列。
}
// 【注】不建议在结点的结构体类中添加 visited 属性,而是另外使用一个 visited 数组来判断,每次遍历时,将 visited 数组置为 false,保证两次遍历之间不影响。
FirstNeighbor(G, x)
和 NextNeighbor(G, x, y)
两个函数找到该顶点的所有邻居顶点的顺序不同,得到的遍历序列也不同。
注意,这个 BFS 是遍历一个连通分量的函数,如果图中有多个彼此不连通的连通分量,则需要对多个连通分量调用 BFS 函数。
对于无向图,调用 BFS 函数的次数 = 连通分量数。
空间复杂度最大需要 ***O(|V|)***,即一个顶点连接了其他所有顶点包括自己,辅助队列需要放 |V| 个。
时间复杂度需要分别分析访问顶点和访问边的时间!
邻接矩阵有 |V| 个点,访问 |V| 个顶点需要 O(|V|) 的时间,每次遍历一个顶点,找其所有邻居需要遍历该行,因此时间复杂度为 O(|V|) + O(|V^2|) = O(|V^2|)
邻接表,访问 |V| 个顶点需要 O(|V|) 的时间,访问他们的邻居需要 O(|E|) 的时间(无向图为 2|E|),因此时间复杂度为: O(|V| + |E|)
对于邻接矩阵:邻接矩阵唯一,广度优先遍历序列唯一。
对于邻接表:邻接表不唯一,广度优先遍历序列不唯一。
将除了第一次遍历的边以外的边去掉,便成为了广度优先生成树,即第一次遍历到某条边,就将其标记,遍历结束后,去掉没有标记的边。
对非连通图的多个连通分量执行广度优先遍历,即可得到,也就是一个连通分量对应一个广度优先生成树。
见王道-书P216-217
其实就是:先初始化一个数组存储从初始结点 u 到某个结点的最短路径长度,在遍历到每个顶点时,记顶点为 v,其邻居为 w,w未被访问,d[w] = d[v] + 1,如此反复即可。
依赖递归工作栈完成
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
void DFSTraverse(Graph G) {
for (int i = 0; i < G.vexnum; i++)
visited[i] = false; // 初始化标记数组
for (i = 0; i < G.vexnum; i++) {
if ( !visited[i]) // 如果连通分量未被遍历
DFS(G, i); // 从 i 开始进行 DFS
}
}
void DFS(Graph G, int v) {
visit(v); // 访问 v
visited[v] = true; // 置为遍历过
for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)) {
if ( !visited[w] ) // 如果 w 是 v 未被遍历过的邻居
DFS(G, w); // 从 w 开始遍历
}
}
空间复杂度最大需要 ***O(|V|)***,即一个顶点依次连接了其他所有顶点,栈中需要放 |V| 个。
时间复杂度需要分别分析访问顶点和访问边的时间!
邻接矩阵有 |V| 个点,访问 |V| 个顶点需要 O(|V|) 的时间,每次遍历一个顶点,找其所有邻居需要遍历该行,因此时间复杂度为 O(|V|) + O(|V^2|) = O(|V^2|)
邻接表,访问 |V| 个顶点需要 O(|V|) 的时间,访问他们的邻居需要 O(|E|) 的时间(无向图为 2|E|),因此时间复杂度为: O(|V| + |E|)
对于邻接矩阵:邻接矩阵唯一,深度优先遍历序列唯一。
对于邻接表:邻接表不唯一,深度优先遍历序列不唯一。
同广度优先生成树,将除了第一次遍历的边以外的边去掉,便成为了深度优先生成树,即第一次遍历到某条边,就将其标记,遍历结束后,去掉没有标记的边。
对非连通图的多个连通分量执行深度优先遍历,即可得到,也就是一个连通分量对应一个深度优先生成树。
对无向图进行 BFS/DFS 遍历,调用 BFS/DFS 的次数 = 连通分量数,对于连通图,只需调用 1 次 BFS/DFS 。
对 有向图 进行 BFS/DFS 遍历,需要具体题目具体分析:
生成树:
与 Dijkstra 算法非常类似!
基本思想:
从某一顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直至所有顶点都纳入为止。
**时间复杂度:**O(|V|^2)
算法实现步骤:
isJoin[size]; // 标记各顶点是否加入生成树
lowCast[size]; // 各个顶点加入树的最低代价
基本思想:
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直至所有顶点都连通。
时间复杂度: O(|E|log2|E|)
算法实现步骤:
无权图可视为权值为 1 的带权图。
/* 借助两个数组完成,d[i] 是从 u 到 i 的最短路径长度,path[i] 记录从 u 到 i 的最短路径中,i 的 前一步是哪一个顶点。逆向输出即可得到轨迹。 */
// 求顶点 u 到其他顶点的最短路径
void BFS_MIN_Distance(Graph G, int u) {
// d[i] 表示从 u 到 i 顶点的最短路径
for (i = 0; i < G.vexnum; i++) {
d[i] = ∞; // 初始化路径长度
path[i] = -1; // i 的前一步是哪个顶点,如果是 -1,则表示其为起点。
}
d[u] = 0; // 起初路径长度为 0
visited[u] = true; // u 设置为遍历过
EnQueue(Q, u); // u 入队
while( !isEmpty(Q) ) {
DeQueue(Q, u); // 队头元素 u 出队
/* w >= 0 是因为 两个查找函数失败后返回 - 1*/
for (w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(Q, u, w)) {
if (!visited[w]) { // w 是 u 未被遍历的邻居顶点
d[w] = d[u] + 1; // 路径长度 + 1
path[w] = u; // 到 w 的前一步是 u
visited[w] = true;
EnQueue(Q, w); // 顶点 w 入队
}//if
}// for
}// while
}
带权路径长度:两个顶点之间的一条路径所经过的边权值之和。
Dijkstra 算法不适用于边权值有负值的情况。
与 Prim 算法思想一致,每次找加入当前路径代价最小的顶点。
**时间复杂度:**O(|V|^2)
算法实现步骤:
【注】算法比较抽象,只可意会不可言传,必须 动手多来几遍(当然是写出 3 个辅助数组,模拟机器运行,而不是画圈圈就完事了) 才能有所感悟。
isfinal[i]; // 标记是否找到了从 u 到 i 的最短路径
dist[i]; // 从 u 到 i 的最短路径长度
path[i]; // 从 u 到 i 的最短路径,i 的前驱结点
isfinal[i]; // 从哪个顶点开始,就将顶点对应的数组值改成 true,其他置为 false。
// isfinal[u] = true; isfinal[i] = false;
dist[i]; // 起始顶点的 dist[u] 设置为 0,根据 u 到其余顶点的距离设置 dist[i]
// 若 u 到 i 有边,则设置为 dist[i] = arcs[u][i],否则设置为 dist[i] = ∞
path[i]; // path[u] 设置为 -1,表示 u 为起始点,若 u 到 i 有边,则设置为 u,无边设置为 -1,待定
// path[i] = (arcs[u][i] == ∞)? -1 : u;
【注】 arcs[u][i] 表示从 u 到 i 的权值
进行 n - 1 轮处理:
① 循环遍历所有顶点,找到 尚未确定最短路径 且 dist 最小的顶点 Vi,将 Vi 的 isfianl[i] 置为 true(②、③完成后,才确定了从 u 到 Vi 的最短路径)。
② 遍历 Vi 的邻居结点。
③ 判断邻居结点 Vj 是否为 尚未确定最短路径(isfinal[j] == false):
dist[j] > dist[i] + arcs[i][j]
),成立,则令 dist[j] = dist[i] + arcs[i][j]; path[j] = i;
邻居结点的前驱结点也设置为 i。前言:
由于每个边的权值有大有小,因此,某两个结点 A、B 的最短路径可能需要经过多个中转点之后得到(尽管 A B 可能是邻居)。如果一个图越复杂,求 A B 的最短路径过程中,可能经过的中转点就越多。
动态规划的思想:
起初,令所有点都不可以是中转点,我们可以得到每两个顶点之间的“当前最短路径”就是记录的边权值。
随后,令一个点 Vi 可以是中转点,判断每两个A B顶点之间: A → Vi → B 与 A → B的距离,更新“边权值”,并且记录 A 到 B 的中转点(如果需要的话)。完成以上操作后,凭借 Vi 为中转点,完成了“边权值”的更新,于是不断新增中转点,便可得到凭借图中每个顶点为中转点的“边权表”,该表即为最短路径了。
Floyed 算法思想:
递 推 产 生 两 个 n 阶 的 方 阵 序 列 : A ( − 1 ) , A ( 0 ) , A ( 1 ) , . . . , A ( k ) , . . . , A ( n − 1 ) 和 P a t h ( − 1 ) , P a t h ( 0 ) , P a t h ( 1 ) , . . . , P a t h ( k ) , . . . , P a t h ( n − 1 ) 其 中 : A ( − 1 ) [ i ] [ j ] 矩 阵 记 录 了 从 v i 到 v j 没 有 以 任 何 顶 点 中 转 点 的 路 径 长 度 A ( 0 ) [ i ] [ j ] 矩 阵 记 录 了 从 v i 到 v j 以 顶 点 v 0 位 中 转 点 的 路 径 长 度 A ( k ) [ i ] [ j ] 矩 阵 记 录 了 从 v i 到 v j 以 顶 点 v 0 、 v 1 、 v 2 、 . . . 、 v k 作 为 中 转 点 的 路 径 长 度 P a t h ( k ) [ i ] [ j ] 矩 阵 记 录 了 A 矩 阵 运 算 时 , v i 到 v j 的 中 转 点 k k 表 示 了 序 号 < = k 顶 点 参 与 了 运 算 算 法 描 述 : ① A ( − 1 ) [ i ] [ j ] = a r c s [ i ] [ j ] ② A ( k ) [ i ] [ j ] = M i n { A ( k − 1 ) [ i ] [ j ] , A ( k − 1 ) [ i ] [ k ] + A ( k − 1 ) [ k ] [ j ] } , k = 0 , 1 , . . . , n − 1 ③ 若 ② 中 A ( k ) [ i ] [ j ] = A ( k − 1 ) [ i ] [ k ] + A ( k − 1 ) [ k ] [ j ] } , 则 P a t h ( k ) [ i ] [ j ] = k , k = 0 , 1 , . . . , n − 1 经 过 n 次 迭 代 , A ( n − 1 ) [ i ] [ j ] 就 是 从 v i 到 v j 的 最 短 路 径 长 度 ( 顶 点 从 v 0 到 v n − 1 ) \begin{array}{l} 递推产生两个\ n\ 阶的方阵序列:{A^{ (- 1)}},{A^{(0)}},{A^{(1)}},...,{A^{(k)}},...,{A^{(n - 1)}}\ \\和\ Pat{h^{( - 1)}},Pat{h^{(0)}},Pat{h^{(1)}},...,Pat{h^{(k)}},...,Pat{h^{(n - 1)}}\\ 其中:\\ {A^{ (- 1)}}[i][j]\ 矩阵记录了从\ {v_i}\ 到\ {v_j}\ 没有以任何顶点中转点的路径长度\\ {A^{(0)}}[i][j]\ 矩阵记录了从\ {v_i}\ 到\ {v_j}\ 以顶点\ {v_0}\ 位中转点的路径长度\\ {A^{(k)}}[i][j]\ 矩阵记录了从\ {v_i}\ 到\ {v_j}\ 以顶点\ {v_0}、{v_1}、{v_2}、...、{v_k}\ 作为中转点的路径长度\\ Pat{h^{(k)}}[i][j]\ 矩阵记录了\ A\ 矩阵运算时,\ {v_i}\ 到\ {v_j}\ 的中转点\ k\\ k\ 表示了序号\ <= k\ 顶点参与了运算\\ \\ 算法描述:①{A^{ (- 1)}}[i][j]=arcs[i][j] \\ \qquad \qquad \ \ ② {A^{ (k)}}[i][j]=Min\{ {A^{(k - 1)}}[i][j],\ {A^{(k - 1)}}[i][k] + {A^{(k - 1)}}[k][j]\} ,\ k = 0,1,...,n - 1 \\ \qquad \qquad \ \ ③若②中{A^{ (k)}}[i][j]={A^{(k - 1)}}[i][k] + {A^{(k - 1)}}[k][j]\},则\ Pat{h^{(k)}}[i][j]=k\ ,\ k = 0,1,...,n - 1 \\ \\ 经过\ n\ 次迭代,{A^{(n - 1)}}[i][j]\ 就是从\ {v_i}\ 到\ {v_j}\ 的最短路径长度(顶点从\ {v_0}\ 到 \ {v_{n - 1}}\ ) \end{array} 递推产生两个 n 阶的方阵序列:A(−1),A(0),A(1),...,A(k),...,A(n−1) 和 Path(−1),Path(0),Path(1),...,Path(k),...,Path(n−1)其中:A(−1)[i][j] 矩阵记录了从 vi 到 vj 没有以任何顶点中转点的路径长度A(0)[i][j] 矩阵记录了从 vi 到 vj 以顶点 v0 位中转点的路径长度A(k)[i][j] 矩阵记录了从 vi 到 vj 以顶点 v0、v1、v2、...、vk 作为中转点的路径长度Path(k)[i][j] 矩阵记录了 A 矩阵运算时, vi 到 vj 的中转点 kk 表示了序号 <=k 顶点参与了运算算法描述:①A(−1)[i][j]=arcs[i][j] ②A(k)[i][j]=Min{A(k−1)[i][j], A(k−1)[i][k]+A(k−1)[k][j]}, k=0,1,...,n−1 ③若②中A(k)[i][j]=A(k−1)[i][k]+A(k−1)[k][j]},则 Path(k)[i][j]=k , k=0,1,...,n−1经过 n 次迭代,A(n−1)[i][j] 就是从 vi 到 vj 的最短路径长度(顶点从 v0 到 vn−1 )
算法程序:
// 初始化 A^-1 矩阵,和 Path^-1 矩阵。
A = acrs;
Path = -1;
//
for (int k = 0; k < n; k++){ // 序号 <= k 的顶点为中转点
for (int i = 0; i < n; i++) { // i 为行号,j为列号,遍历矩阵
for (int j = 0; j < n; j++) {
if (A[i][j] > A[i][k] + A[k][j]){ // 以 vk 为中转点的路径更短
A[i][j] = A[i][k] + A[k][j]; // 更新最短路径长度
Path[i][j] = k; // i 到 j 的中转点加 k。
}
}
}
}
有向无环图(DAG——Directed Acyclic Graph)
一个有向图中不存在环,则称为有向无环图。
AOV 网(Activity On Vertex Network,用顶点表示活动的网):
用 DAG 图(有向无环图)表示一个工程,顶点表示活动,有向边
表示活动 Vi 必须先于 Vj 进行。
【注】采用层序遍历来获得拓扑排序的序列是错误的!!!
例如:下面这个图,用层序遍历得到了错误结果!
算法思想:
***算法程序:***邻接表法
// 用邻接表
#define MaxVertexNum 100
typedef struct ArcNode { // 边表结点
int adjvex; // 该弧所指向的顶点的位置
struct ArcNode *next; // 指向下一条弧的指针
InfoType info; // 网的边权值
}ArcNode;
typedef struct VNode {
VertexType data; // 顶点信息
ArcNode *first; // 指向第一条依附该顶点的弧的指针
}VNode, AdjList[MaxVertexNum];
typedef struct ALGraph {
AdjList vertices; // 邻接表
int vexnum, arcnum; // 图的顶点数和弧数
}ALGraph;
// let's go
indegree[G.vexnum]; // 保存 G 中每个顶点的入度 indegree[i] 即顶点 i 的入度
print[G.vexnum]; // 保存拓扑排序的序列,初始化全部为 -1
SqStack S; // 栈 S 保存 G 中每一步入度为 0 的顶点(用队列也可)
bool TopologialSort(Graph G) {
InitStack(S);
for (int i = 0; i < G.vexnum; i++)
if (indegree[i] == 0)
Push(S, i); // 将所有入度为 0 的顶点入栈,其余顶点的入度 >= 1
int count = 0; // 计数,记录当前已经输出的顶点数
while ( !isEmpty(S) ) { // 栈非空,则存在入度为 0 的顶点
Pop(S, i); // 栈顶元素出栈,循环到此时,入度为 0 的顶点出栈
print[count++] = i; // 输出顶点 i,先输出后 ++
/* 遍历 i 的邻居结点,令 i 的所有邻居顶点 v 的入度均 - 1,即“删除 i” */
/* for 中也可写 p = FirstNeighbor(G, i); p >= 0; p = NextNeighbor(G, i, p)
不过注意此时 p 就是序号了,后续也要改 */
for (p = G.vertices[i].first; p ; p = p.next) {
v = p -> adjvex; // p 指针指向的 v 结点
indegree[v]--; // 将所有邻居顶点 v 的入度 - 1,即完成了删除 i 的操作
if (indegree[v] == 0)
Push(S, v); // 入栈入度为 0 的顶点
}// for
}// while
if (count < G.vexnum)
return false; // 有向图中有回路,排序失败。
else
return true; // 拓扑排序成功
}
算法思想:
参考下面用 DFS 实现 逆拓扑排序的方法,初始化一个栈,每次遍历到“没有可以再遍历的邻居结点时”,将该顶点入栈,待到全部遍历结束后,将栈内元素依次弹出输出即可。
【注】也就是将逆拓扑排序的结果逆向输出。
参考 22 版本王道书 P252
算法思想:
***算法程序:***邻接表法
// 用邻接表
#define MaxVertexNum 100
typedef struct ArcNode { // 边表结点
int adjvex; // 该弧所指向的顶点的位置
struct ArcNode *next; // 指向下一条弧的指针
InfoType info; // 网的边权值
}ArcNode;
typedef struct VNode {
VertexType data; // 顶点信息
ArcNode *first; // 指向第一条依附该顶点的弧的指针
}VNode, AdjList[MaxVertexNum];
typedef struct ALGraph {
AdjList vertices; // 邻接表
int vexnum, arcnum; // 图的顶点数和弧数
}ALGraph;
// let's go
outdegree[G.vexnum]; // 保存 G 中每个顶点的出度 outdegree[i] 即顶点 i 的出
print[G.vexnum]; // 保存逆拓扑排序的序列,初始化全部为 -1
SqStack S; // 栈 S 保存 G 中每一步出度为 0 的顶点(用队列也可)
bool TopologialReverseSort(Graph G) {
InitStack(S);
for (int i = 0; i < G.vexnum; i++)
if (outdegree[i] == 0)
Push(S, i); // 将所有出度为 0 的顶点入栈
int count = 0; // 计数,记录当前已经输出的顶点数
while ( !isEmpty(S) ) { // 栈非空,则存在出度为 0 的顶点
Pop(S, i); // 栈顶元素出栈,循环到此时,出度为 0 的顶点出栈
print[count++] = i; // 输出顶点 i,先输出后 ++
/* 遍历 i 的邻居结点,令 i 的所有邻居顶点 v 的出度均 - 1,即“删除 i” */
/* FirstFrom 返回 第一个终点是 i 的顶点,NextFrom 则是下一个 */
for (v = FirstFrom(G, i); v >= 0; v = NextFrom(G, i, v)) {
indegree[v]--; // 将所有 v 的出度 - 1,即完成了删除 i 的操作
if (indegree[v] == 0)
Push(S, v); // 入栈出度为 0 的顶点
}// for
}// while
if (count < G.vexnum)
return false; // 有向图中有回路,逆排序失败。
else
return true; // 逆拓扑排序成功
}
算法思想:
DFS 是深度优先遍历,也叫做先根遍历,DFS 每次都要从一个顶点往下不断找“根”,直到找到一颗子树最深的根结点,此时该结点的 DFS 调用结束,将返回其祖先,从其祖先的邻居结点开始继续调用 DFS,对它们调用的 DFS,也会找到最深的根结点,然后层层向上输出,因此保证了流程的顺序正确性。
void DFSTraverse(Graph G) {
for (int i = 0; i < G.vexnum; i ++) {
visited[i] = 0;
}
for (int v = 0; v < G.vexnum; v ++) {
DFS(G, v);
}
}
void DFS(Graph G, int v) {
visited[v]++; // 访问一次便 ++
for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, w, v)) {
if (visited[w] == 0)// 未被遍历过
DFS(G, w);
if (visited[w] >= 2)// 被访问两次甚至更多次,则说明,图中存在环路,逆拓扑失败
print("逆拓扑排序失败");
}
print(v); // 输出顶点 v
}
AOE 网 (Activity On Edge NetWork):
用边表示活动的网络
有向无环图
边代表活动,边有权值,权值表示完成活动的开销,活动需要花费一定的时间。
顶点代表事件,事件在其前驱活动全部完成的情况下,瞬间发生,瞬间结束,因此事件更像是一种标志,一种象征。
特点:
① 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
② 只有在进入某顶点的各有向边所代表的的活动都已结束时,该顶点所代表的的事件才能发生。有些活动可以并行。
③ 仅有一个入度为 0 的顶点,称为 开始顶点(源点),表示整个工程的开始;
④ 仅有一个出度为 0 的顶点,称为 结束顶点(汇点),表示整个工程的结束。
关键路径和关键活动:
从源点到汇点的所有路径中,具有最大路径长度的路径称为 关键路径,关键路径上的活动称为 关键活动,关键活动影响了整个工程的时间,若不能按时完成关键活动,工程时间则会延长,因此 完成整个工程的最短时间就是关键路径的长度。
参量定义:
注意点——缩短工期:
可以通过 适当加快 关键活动 来达到缩短工期的目的,但是一旦某关键活动缩短到一定程度,它可能变为非关键活动。
关键路径不唯一,对于有多条关键路径的网,缩短工期的手段:
①只加快 包括在所有关键路径上的关键活动 。
②同时加快第一个多条关键路径分叉口的 多个分叉关键路径,保证多条关键路径均缩短。
求解——关键路径长度、关键路径:
算法步骤
从源点出发,先对 AOE 网进行拓扑排序,得到事件的先后顺序。
按照拓扑排序的序列,求每个事件的最早开始时间 ve(k)
源点 ve(源点) = 0;
其余结点 k 的 ve(k) 值 = Max{ k 结点的前驱 j 的最早开始时间 ve(j) + 两点之间的权值 Weight
从汇点出发,进行逆拓扑排序。
按照逆拓扑排序的序列,求每个事件的最迟开始时间 vl(k)
汇点 vl(汇点) = ***ve(汇点)***,汇点一定是关键活动。
其余结点 vl(k) = Min{ k 结点的后继结点 j 的最迟开始时间 vl(j) - 两点之间的权值 Weight
根据 **ve(k),求 e(i) = 该活动弧尾(弧的起点)表示的事件最早开始时间。
根据 **vl(k),求 l(i) = 该活动弧头(弧的终点)表示的事件最迟开始的时间 - 该活动的时间。
求 ***d(i)***,并找出 d(i) 为 0 的活动,即为关键活动。
找本质,看题目是探究图的什么性质,如连通与否,如何遍历,遍历中进行怎样的操作?
void BFSfunTraverse(G) {
for (int i = 0; i < G.vexnum; i ++) {
visited[i] = false;
}
for (i = 0; i < G.vexnum; i++) {
if(! visited[i])
BFSfun(G, i);
}
}
void BFSfun(G, u) { //G 为图或者树, u 为图、树开始的 点
// 初始化你的辅助数组
LinkQueue Q;
InitQueue(Q);
// 如果是图或者多叉树,需要借助 visited 数组
visited[u] = true; // u 设置为遍历过
EnQueue(Q, u); // u 入队
while( !isEmpty(Q) ) {
DeQueue(Q, u); // 队头元素 u 出队
// 接下来是,遍历 u 的邻居顶点内容
/* 1. 如果是 二叉树,则不需要循环,只需要 rchild 和 lchild 入队即可。*/
/* 2. 如果是图或者多叉树,则需要遍历 出队点 u 的所有邻居,并每次递归调用本函数。*/
/* 下面是图/多叉树的写法 w >= 0 是因为 两个查找函数失败后返回 - 1*/
for (w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(Q, u, w)) {
if (!visited[w]) { // w 是 u 未被遍历的邻居顶点
// 这里写你针对 w 的其他处理操作
// 这里写你针对 w 的其他处理操作
visited[w] = true;
EnQueue(Q, w); // 顶点 w 入队
// 这里写你针对 w 的其他处理操作
// 这里写你针对 w 的其他处理操作
}//if
}// for
}// while
}
【注】
上述针对某个结点的操作,是写在“紧接在队头元素出队之后”,还是“队头出队后,对每一个邻居结点遍历时”有所区别,后者不对“图”或“树”的第一个结点进行操作,而是从除第一个结点以外的结点开始操作,前者则对所有结点都进行操作,在 BFS 求单源无权值最短路径时,需使用后者进行操作。
图或者多叉树需要注意,visited 数组,判断当前是否遍历过该顶点。
对 无向图 进行 BFS/DFS 遍历,调用 BFS/DFS 的次数 = 连通分量数,对于连通图,只需调用 1 次 BFS/DFS 。
对 有向图 进行 BFS/DFS 遍历,需要具体题目具体分析: