数据结构——图

图(Graph)

文章目录

  • 图(Graph)
  • 图的基本概念
    • 图的定义
      • 有向图
      • 无向图
      • 简单图、多重图
      • 顶点的度、入度和出度
      • 路径、路径长度、回路、简单路径、简单回路、连通、强连通
      • 子图
      • 连通、连通图、连通分量
      • 生成树——针对连通图(无向图)
      • 生成森林——针对连通图(无向图)
      • 边的权、带权图(网)、带权路径长度
      • 完全图、稀疏图、稠密图
      • 有向树
    • ⭕图的性质
  • 图的存储及基本操作
    • 邻接矩阵法
      • 不带权的图
      • 对于带权图
      • 邻接矩阵的性质
    • 邻接表法(顺序+链式)
    • 十字链表法——只能存储有向图
    • 邻接多重表——只能存储无向图
    • 图的基本操作——邻接矩阵和邻接表
        • `Adjacent(G, x, y)` 判断图 G 是否存在边 `

图的基本概念

图的定义

图可以由顶点集和边集组成

V:(Vertex) 顶点集,eg:{A, B, C, D}

E、Arc:(Edge、Arc弧) 边集,eg:{(A, B), (B, C), (C, D), (D, A)}

  • 顶点两头可以没有边,边的两头必须连接顶点
  • 一个图至少要有一个顶点

有向图

E 是有向边的集合,一条边是两个顶点的有序对,也称为

记 顶点 v 到顶点 w 的弧 为 ,其中 v 是弧头w 为弧尾,注意用尖括号表示有向边,也称 v 邻接到 w,或 w 邻接自 v。 !=


无向图

E 是无向边的集合,一条边是两个顶点的无序对,也称为

记 顶点 v 到顶点 w 的弧 为 (v, w),注意用圆括号表示有向边,也称 顶点 w 和顶点 v 互为邻接点,边 (v, w) 依附于顶点 w 和 v,或边 (v, w) 和顶点 v、w 相关联。


简单图、多重图

只研究简单图

  1. 不存在重复边
  2. 不存在顶点到自身的边


顶点的度、入度和出度

无向图:

度——依附于顶点边的条数,记为 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=1nTD(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=1nID(vi)=i=1nOD(vi)=e

路径、路径长度、回路、简单路径、简单回路、连通、强连通

  • 路径――顶点v,到顶点v之间的一条路径是指顶点序列,VpVi,v, ,.0,vim , vq。

  • 路径长度――路径上边的数目。

  • 回路――第一个顶点和最后一个顶点相同的路径称为回路或环。

  • 简单路径――在路径序列中,顶点不重复出现的路径称为简单路径。

  • 简单回路――除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。

  • 点到点的距离――从顶点 u 出发到顶点 v 的最短路径若存在,则此路径的长度称为从 u 到 v 的距离。若从 u 到 v 根本不存在路径,则记该距离为无穷*(∞)* 。

  • 无向图中,若从顶点 v 到顶点 w 有路径存在,则称 v 和 w 是连通的。

  • 有向图中,若从顶点 v 到顶点 w 和从顶点 w 到顶点 v 之间都有路径,则称这两个顶点是强连通


子图

有向图与无向图同理
数据结构——图_第1张图片


连通、连通图、连通分量

无向图

  • 连通图——图中任意两个顶点都是连通的
  • 连通分量——极大连通子图,不止一个

有向图

  • 强连通图——图中任意两个顶点都是强连通的(两个方向均能找到路径)
  • 强连通分量——极大强连通子图,不止一个


生成树——针对连通图(无向图)

生成树

包含图中全部顶点的一个极小连通子图

  1. 包含全部顶点
  2. 若图中顶点数为 n,则生成树有 n - 1 条边


生成森林——针对连通图(无向图)

生成森林

非连通图中,连通分量的生成树构成生成森林。


边的权、带权图(网)、带权路径长度

每条边有权值

带权路径长度:当图是权图时,一条路径上所有边的权值之和,称为该路径的带权路径和长度。



完全图、稀疏图、稠密图

有   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(n1)  An2=n(n1)  E<VlogV 


有向树

一个顶点的入度为 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=1nTD(vi)=2eTD(v)=ID(v)+OD(v) n e i=1nID(vi)=i=1nOD(vi)=e n  G G  n1 GCn12 n1  n  G G  n  Cn2=2n(n1)  An2=n(n1) 


图的存储及基本操作

邻接矩阵法

不带权的图

#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|)

  • 存储有向图时,计算度、入度需遍历全部边结点,比较繁琐。

记住下面这个图,比较抽象,仔细体悟吧!

数据结构——图_第2张图片


十字链表法——只能存储有向图

针对邻接表存储有向图时,计算度、入度比较繁琐,采用十字链表存储有向图。
数据结构——图_第3张图片

// 边集,也是弧集, 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,即从 VexNodefirstOut 开始遍历,每次寻找其 tlink 即可

计算入度:

将该顶点视为 head,即从 VexNodefirstIn 开始遍历,每次寻找其 hlink 即可

空间复杂度:O(|V| + |E|)


邻接多重表——只能存储无向图

针对 无向图,若使用邻接表,则进行删除等操作时,由于信息冗余——边的信息有两份,需要删除两个顶点的边信息,比较繁琐,故使用邻接多重表。

数据结构——图_第4张图片

// 边集,也是弧集,(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;	// 点数和边数
}

计算度:

从顶点 VexNodefirstEdge 出发,找到一个弧时,计数,并判断其 ivexjvex ,哪个存储的位置 = 该弧的位置,随后对应找其 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) 或 有向边 不存在,则向 图 G 中添加该边。

无向图:

  • ***邻接矩阵:***注意vexnum + 1

  • ***邻接表:***遍历 x 结点,使用头插法(尾插法复杂度高)


有向图:

  • ***邻接矩阵:***同上

  • ***邻接表:***同上


RemoveEdge(G, x, y) 若无向边 (x, y) 或 有向边 存在,则在 图 G 中删除。

无向图:

  • ***邻接矩阵:***找到 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) 或 的权值为 v。

Adjacent(G, x, y) 用一用嘛



图的遍历

广度优先搜索(BFS——Breadth-First-Search)

与树的广度优先遍历类似~依赖辅助队列完成

  • 树每次找的是左右孩子,不存在重复访问可能。

  • 图找该顶点的所有邻居顶点,但存在重复访问的可能性

  • 注意,有向图中,一个结点的邻居顶点是有方向的,在有向图中,有时候需要多次执行 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|)

  • 对于邻接矩阵:邻接矩阵唯一,广度优先遍历序列唯一。

  • 对于邻接表:邻接表不唯一,广度优先遍历序列不唯一。


广度优先生成树

将除了第一次遍历的边以外的边去掉,便成为了广度优先生成树,即第一次遍历到某条边,就将其标记,遍历结束后,去掉没有标记的边。

  • 对于邻接矩阵:邻接矩阵唯一,广度优先遍历序列唯一,广度优先生成树唯一。
  • 对于邻接表:邻接表不唯一,广度优先遍历序列不唯一,广度优先生成树不唯一。


广度优先生成森林

对非连通图的多个连通分量执行广度优先遍历,即可得到,也就是一个连通分量对应一个广度优先生成树。


BFS 算法求解单源最短路径问题

见王道-书P216-217

其实就是:先初始化一个数组存储从初始结点 u 到某个结点的最短路径长度,在遍历到每个顶点时,记顶点为 v,其邻居为 w,w未被访问,d[w] = d[v] + 1,如此反复即可。



深度优先搜索(Depth-First-Search, DFS)

依赖递归工作栈完成

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 遍历,需要具体题目具体分析:

  • 在一个连通图内,如果起始顶点到其他顶点都有路径(能走到),则只需调用 1 次。
  • 对于强连通图,从任一结点出发都只需要调用 1 次 BFS/DFS。



图的应用

生成树:

  • 针对对象为连通图
  • 包含图中全部顶点的一个极小连通子图
  • 图的顶点数为 n,生成树含有 n - 1 条边
  • 生成树砍去一边,就变成非连通图,加上一边,必成回路

最小生成树(最小代价树)(MST, Minimum-Spanning-Tree)

  • 同一个图可能有多种最小生成树。
  • 各边权值互不相等时,最小生成树唯一。
  • Prim 算法和 Kruskal 算法都基于贪心算法。

Prim (普利姆)算法

与 Dijkstra 算法非常类似!

基本思想:

从某一顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直至所有顶点都纳入为止。


**时间复杂度:**O(|V|^2)


算法实现步骤:

  1. 初始化两个数组
isJoin[size];	// 标记各顶点是否加入生成树
lowCast[size];	// 各个顶点加入树的最低代价
  1. 循环遍历两个数组,找到 lowCast 最低的,并且还未加入树的顶点,将其纳入生成树中。
  2. 顶点纳入生成树后,循环遍历,lowCast 随之发生变化,更新 lowCast 数组(循环到第一个未加入生成树的顶点,判断其与刚新加入的顶点之间是否有边,若有,且比先前的 lowCast 值小,则更新 lowCast 值)。
  3. 重复 2 和 3 直至完成。



Kruskal (克鲁斯卡尔)算法

基本思想:

每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直至所有顶点都连通。


时间复杂度: O(|E|log2|E|)


算法实现步骤:

  1. 将各条边按照权值从小到大排序,排序时要存储边的信息(两个端点)
  2. 检查依次每一条边的两个顶点是否连通(是否属于同一个集合——并查集,时间复杂度为 O(log2|E|)),如果不属于一个集合,就将这两个顶点连起来。
  3. 重复 2 直至完成。


最短路径

BFS 算法——单源最短路径、无权图

无权图可视为权值为 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 算法——单源最短路径、带权图

  • 带权路径长度:两个顶点之间的一条路径所经过的边权值之和。

  • Dijkstra 算法不适用于边权值有负值的情况

  • 与 Prim 算法思想一致,每次找加入当前路径代价最小的顶点。

**时间复杂度:**O(|V|^2)


算法实现步骤:

【注】算法比较抽象,只可意会不可言传,必须 动手多来几遍(当然是写出 3 个辅助数组,模拟机器运行,而不是画圈圈就完事了) 才能有所感悟。

  1. 依赖 3 个辅助数组:在循环中不断变化
isfinal[i];	// 标记是否找到了从 u 到 i 的最短路径
dist[i];	// 从 u 到 i 的最短路径长度
path[i];	// 从 u 到 i 的最短路径,i 的前驱结点
  1. 初始化三个数组,即设置好未进行操作时:是否找到 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 的权值
  1. 进行 n - 1 轮处理:

    ① 循环遍历所有顶点,找到 尚未确定最短路径dist 最小的顶点 Vi,将 Vi 的 isfianl[i] 置为 true(②、③完成后,才确定了从 u 到 Vi 的最短路径)。

    ② 遍历 Vi 的邻居结点。

    ③ 判断邻居结点 Vj 是否为 尚未确定最短路径(isfinal[j] == false)

    • 若是 → 且 Vj 与 Vi 之间有边 → 若有 先前记录的从 u 到 邻居结点 Vj 的距离 > 从 u 到 i 再到 j 的距离dist[j] > dist[i] + arcs[i][j] ),成立,则令 dist[j] = dist[i] + arcs[i][j]; path[j] = i; 邻居结点的前驱结点也设置为 i


Floyed 算法——各个顶点之间的最短路径、带权图

前言

由于每个边的权值有大有小,因此,某两个结点 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(n1)  Path(1),Path(0),Path(1),...,Path(k),...,Path(n1)A(1)[i][j]  vi  vj A(0)[i][j]  vi  vj  v0 A(k)[i][j]  vi  vj  v0v1v2...vk Path(k)[i][j]  A  vi  vj  kk  <=k A(1)[i][j]=arcs[i][j]  A(k)[i][j]=Min{A(k1)[i][j], A(k1)[i][k]+A(k1)[k][j]}, k=0,1,...,n1  A(k)[i][j]=A(k1)[i][k]+A(k1)[k][j]} Path(k)[i][j]=k , k=0,1,...,n1 n A(n1)[i][j]  vi  vj  v0  vn1 

算法程序:

// 初始化 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。
            }
        }
    }
}


三种算法总结

数据结构——图_第5张图片



有向无环图(DAG)描述表达式

有向无环图(DAG——Directed Acyclic Graph)

一个有向图中不存在环,则称为有向无环图。

  • 顶点中不可能存在重复的操作数
  1. 把各个操作数不重复地排成一排,放在叶子结点位置。
  2. 标出各个运算符的生效顺序。
  3. 按第二步中的顺序加入运算符,注意“分层”
  4. 从底向上逐层检查同层的运算符是否可以合体(判断运算符和操作数是否一致)。


拓扑排序

AOV 网(Activity On Vertex Network,用顶点表示活动的网):

  • 有向无环图——DGA(拓扑排序深度优先遍历、关键路径(第一步是拓扑排序)可以检验图是否有环
  • 用顶点表示活动
  • 边无权值,仅表示顶点之间的先后关系
  • 若有向无环图的拓扑序列唯一,则 不可以 唯一确定该图,见 22 王道书 P245 19
  • 若用邻接矩阵表示一个有向图,其邻接矩阵是三角矩阵,则存在拓扑排序
  • 若一个有向图的顶点不能排成一个拓扑序列,则判定该有向图 含有定点数大于 1 的强连通分量

用 DAG 图(有向无环图)表示一个工程,顶点表示活动,有向边 表示活动 Vi 必须先于 Vj 进行。

【注】采用层序遍历来获得拓扑排序的序列是错误的!!!

例如:下面这个图,用层序遍历得到了错误结果!

数据结构——图_第6张图片

拓扑排序

非 DFS

算法思想:

  1. 从 AOV 网中选择一个没有前驱的顶点并输出。
  2. 从网中删除该顶点所有以它为起点的有向边
  3. 重复 1 和 2 直至 AOV 网为空,或当前网中不存在 无前驱的顶点 为止,后者表示有向图中存在回路。
  • 每一步都会有入度为 0 的顶点,用栈 / 队列存储这些顶点,出栈时,输出这些顶点,使用类似“层序遍历”的思想,进行拓扑排序。


***算法程序:***邻接表法

// 用邻接表
#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 实现拓扑排序

算法思想:

  1. 参考下面用 DFS 实现 逆拓扑排序的方法,初始化一个栈,每次遍历到“没有可以再遍历的邻居结点时”,将该顶点入栈,待到全部遍历结束后,将栈内元素依次弹出输出即可。

    【注】也就是将逆拓扑排序的结果逆向输出。

  2. 参考 22 版本王道书 P252

    • 若 u 是 v 的祖先,则在调用 DFS 访问 u 时,结束前,必然会先结束对 v 的 DFS,即 v 的 DFS 函数结束时间早于 u。因此只要在DFS 调用过程中增加时间标记,每一个结点 DFS 调用结束后,便计时,使用一个辅助数组存储时间,最后将时间排序,即可得到输出顺序。



逆拓扑排序

非 DFS

算法思想:

  1. 从 AOV 网中选择一个没有后继的顶点并输出。
  2. 从网中删除该顶点和所有以它为终点的有向边
  3. 重复 1 和 2 直至 AOV 网为空,或当前网中不存在 无后继的顶点 为止,后者表示有向图中存在回路。(每一步都会有入度为 0 的顶点)
  • 每一步都会有出度为 0 的顶点,用栈 / 队列存储这些顶点,出栈时,输出这些顶点。
  • 若使用 逆邻接表,则会简化时间。


***算法程序:***邻接表法

// 用邻接表
#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,对它们调用的 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 的顶点,称为 结束顶点(汇点),表示整个工程的结束。


关键路径和关键活动

从源点到汇点的所有路径中,具有最大路径长度的路径称为 关键路径,关键路径上的活动称为 关键活动,关键活动影响了整个工程的时间,若不能按时完成关键活动,工程时间则会延长,因此 完成整个工程的最短时间就是关键路径的长度

参量定义:

  • 事件的最早发生时间(vertex earliest)—— ***ve(k)***,决定了所有从 vk 顶点开始的活动 e1、e2…能够开工的最早时间。
  • 事件的最迟发生时间(vertex latest)—— ***vl(k)***,是保证后续事件 vj 在其最迟发生时间 vl(j) 能发生时,该事件最迟必须发生的时间。
  • 活动 ai 的最早开始时间(earliest)—— e(i) = 该活动弧尾(弧的起点)表示的事件最早开始时间。
  • 活动 ai 最迟开始的事件(latest)—— l(i) = 该活动弧头(弧的终点)表示的事件最迟开始的时间 - 该活动的时间。
  • 活动 ai 的时间余量—— d(i) = 活动 ai 的最迟开始时间 - 最早开始时间 = ***l(i) - e(i)***。若为 0 ,则表示该活动是关键活动,最迟开始时间 = 最早开始时间,刻不容缓!


注意点——缩短工期

  1. 可以通过 适当加快 关键活动 来达到缩短工期的目的,但是一旦某关键活动缩短到一定程度,它可能变为非关键活动。

  2. 关键路径不唯一,对于有多条关键路径的网,缩短工期的手段:

    ①只加快 包括在所有关键路径上的关键活动

    ②同时加快第一个多条关键路径分叉口多个分叉关键路径,保证多条关键路径均缩短。


求解——关键路径长度、关键路径:

  1. 关键路径长度——求出汇点(结束顶点)对应事件的最早开始时间即为关键路径长度。
  2. 关键路径——如果题目没有要求,可以不必求出每个关键活动,在求每个事件的最早开始时间时,记录下每次 Max 的路径,即可找到关键路径。

算法步骤

  1. 从源点出发,先对 AOE 网进行拓扑排序,得到事件的先后顺序。

    按照拓扑排序的序列,求每个事件的最早开始时间 ve(k)

    源点 ve(源点) = 0;

    其余结点 k 的 ve(k) 值 = Max{ k 结点的前驱 j 的最早开始时间 ve(j) + 两点之间的权值 Weight }

    • 前事件最早+该边,找最大的意义是留充足时间,保证 k 前面事件全部完成
  2. 从汇点出发,进行逆拓扑排序

    按照逆拓扑排序的序列,求每个事件的最迟开始时间 vl(k)

    汇点 vl(汇点) = ***ve(汇点)***,汇点一定是关键活动。

    其余结点 vl(k) = Min{ k 结点的后继结点 j 的最迟开始时间 vl(j) - 两点之间的权值 Weight }

    • 后事件最迟 - 该边,找最小的意义是 不推迟进度,保证后续事件能按时完成
  3. 根据 **ve(k),求 e(i) = 该活动弧尾(弧的起点)表示的事件最早开始时间。

  4. 根据 **vl(k),求 l(i) = 该活动弧头(弧的终点)表示的事件最迟开始的时间 - 该活动的时间。

  5. 求 ***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 遍历,需要具体题目具体分析:

  • 在一个连通图内,如果起始顶点到其他顶点都有路径(能走到),则只需调用 1 次。
  • 对于强连通图,从任一结点出发都只需要调用 1 次 BFS/DFS。


你可能感兴趣的:(数据结构,数据结构)