数据结构学习笔记——第6章 图

6 图

6.1 图的基本概念

6.1.1 图的定义

  • 图 G 由顶点集 V 和边集 E 组成,记为 G = ( V , E ) G=(V, E) G=(V,E),其中 V(G) 表示图 G 中顶点的有限非空集;E(G) 表示图 G 中顶点之间的关系(边)的集合
  • V = { v 1 , v 2 , . . . , v n } V = \{ v_1, v_2, ..., v_n \} V={v1,v2,...,vn},则用 ∣ V ∣ | V | V 表示图 G 中顶点的个数,也称图 G 的 E = { ( u , v ) ∣ u ∈ V , v ∈ V } E = \{ ( u,v ) | u \in V, v \in V \} E={(u,v)uV,vV},用 ∣ E ∣ | E | E 表示图 G 中边的条数
  • 线性表、树都可以为空,但图不能为空
  • 有向图
    • 若 E 是有向边(也称)的有限集合,则图 G 为有向图
    • 弧是顶点的有序对,记为 ⟨ v , w ⟩ \left \langle v,w \right \rangle v,w,其中 v, w 是顶点,v 称为弧尾,w称为弧头 ⟨ v , w ⟩ \left \langle v,w \right \rangle v,w 称为从顶点 v 到顶点 w 的弧,也称 v 邻接到 w,或 w 邻接自 v
  • 无向图
    • 若 E 是无向边(简称)的有限集合,则图 G 为无向图
    • 边是顶点的无序对,记为 ( v , w ) ( v,w ) (v,w) ( w , v ) ( w,v ) (w,v),因为 ( v , w ) = ( w , v ) ( v,w )=( w,v ) (v,w)=(w,v),其中 v, w 是顶点,可以说顶点 w 和顶点 v 互为邻接点,边 ( v , w ) ( v,w ) (v,w) 依附于顶点 w 和 v,或者说边 ( v , w ) ( v,w ) (v,w) 和顶点 w, v 相关联
  • 简单图
    • 一个图 G 若满足:① 不存在重复边;② 不存在顶点到自身的边,则称图 G 为简单图
    • 数据结构中仅讨论简单图
  • 多重图
    • 若图 G 中某两个点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则 G 为多重图
  • 完全图(也称简单完全图)
    • 对于无向图, ∣ E ∣ | E | E 的取值范围是 0 0 0 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2,有 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2 条边的无向图称为无向完全图,在完全图中任意两个顶点之间都存在边
    • 对于有向图, ∣ E ∣ | E | E 的取值范围是 0 0 0 n ( n − 1 ) n(n-1) n(n1),有 n ( n − 1 ) n(n-1) n(n1) 条边的无向图称为有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧
  • 子图
    • 设有两个图 G = ( V , E ) G=(V, E) G=(V,E) G ′ = ( V , ′ E ′ ) G'=(V,' E') G=(V,E),若 V ′ V' V V V V 的子集,若 E ′ E' E E E E 的子集,则称 G ′ G' G G G G子图
    • 若有满足 V ′ ( G ) = V ( G ) V'(G) = V(G) V(G)=V(G) 的子图 G ′ G' G ,则称其为 G G G生成子图
  • 连通、连通图和连通分量
    • 无向图中,若从顶点 v 到顶点 w 有路径存在,则称 v 和 w 是连通
    • 若图 G 中任意两个顶点都是连通的,则称图 G 为连通图,否则称为非连通图
    • 无向图中的极大连通子图称为连通分量
    • n 个顶点的连通图最少有 n-1 条边
  • 强连通图、强连通分量
    • 有向图中,若从顶点 v 到顶点 w 和顶点 w 到顶点 v之间都有路径,则称这两个顶点是强连通
    • 若图中任何一对顶点都是强连通的,则称此图为强连通图
    • 有向图中的极大强连通子图称为有向图的强连通分量
    • n 个顶点的连通图最少有 n 条边
  • 生成树、生成森林
    • 连通图的生成树是包含图中全部顶点的一个极小连通子图
    • 若图中顶点数为 n,则它的生成树含有 n-1 条边
    • 对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会变形成一个回路
    • 在非连通图中,连通分量的生成树构成了非连通图的生成森林
  • 顶点的度、入度和出度
    • 图中每个顶点的定义为以该顶点为一个端点的边的数目
    • 对于无向图,顶点 v 的是指依附于该顶点的边的条数,记为 TD(v)
    • 在具有 n 个顶点、e 条边的无向图中, ∑ i = 1 n T D ( v i ) = 2 e \sum_{i=1}^{n}TD(v_i) = 2e i=1nTD(vi)=2e,即无向图的全部顶点的度的和等于边数的两倍,因为每条边和两个顶点相关联
    • 对于有向图,顶点 v 的度分为入度出度,入度是以顶点 v 为终点的有向边的数目,记为 I D ( v ) ID(v) ID(v);而出度是以顶点 v 为起点的有向边的条数,记为 O D ( v ) OD(v) OD(v)。顶点 v 的度等于其入度和出度之和,即 T D ( v ) = I D ( v ) + O D ( v ) TD(v) = ID(v) + OD(v) TD(v)=ID(v)+OD(v)
    • 在具有 n 个顶点、e 条边的无向图中, ∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = e \sum_{i=1}^{n}ID(v_i) = \sum_{i=1}^{n}OD(v_i) = e i=1nID(vi)=i=1nOD(vi)=e,即有向图的全部顶点的入度之和与出度之和相等,并且等于边数。这是因为每条有向边都有一个起点和终点
  • 边的权和网
    • 在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值
    • 这种边上带有权值的图称为带权图,也称
  • 稠密图、稀疏图
    • 边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的
    • 一般当图 G 满足 ∣ E ∣ < ∣ V ∣ l o g ∣ V ∣ |E| < |V| log|V| E<VlogV 时,可以将 G 视为稀疏图
  • 路径、路径长度和回路
    • 顶点 v p v_p vp 到顶点 v q v_q vq 之间的一条路径是指顶点序列 v p , v i 1 , v i 2 , . . . , v i m , v q v_p, v_{i_1}, v_{i_2}, ..., v_{i_m}, v_q vp,vi1,vi2,...,vim,vq,当然,关联的边也可以理解为路径的构成要素
    • 路径上边的数目称为路径长度
    • 第一个顶点和最后一个顶点相同的路径称为回路
    • 若一个图有 n 个顶点,并且有大于 n-1 条边,则此图一定有环
  • 简单路径、简单回路
    • 在路径序列中,定点不重复出现的路径称为简单路径
    • 除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路
  • 距离
    • 从顶点 u 出发到顶点 v 的最短路径若存在,则此路径的长度称为从 u 到 v 的距离
    • 若从顶点 u 到顶点 v 根本不存在路径,则记该距离为无穷( ∞ \infty
  • 有向树
    • 一个顶点的入度为 0、其余顶点的入度均为 1 的有向图,称为有向树

6.2 图的存储及基本操作

6.2.1 邻接矩阵法

  • 所谓邻接矩阵存储,是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即可顶点直接的邻接关系),存储顶点直接邻接关系的二维数组称为邻接矩阵
  • 结点数为 n n n 的图 G = ( V , E ) G = (V, E) G=(V,E) 的邻接矩阵 A A A n × n n×n n×n 的。将 G G G 的顶点编号为 v 1 , v 2 , . . . , v n v_1, v_2, ..., v_n v1,v2,...,vn。若 ( v i , v j ) ∈ E (v_i, v_j) \in E (vi,vj)E,则 A [ i ] [ j ] = 1 A[i][j] = 1 A[i][j]=1,否则 A [ i ] [ j ] = 0 A[i][j] = 0 A[i][j]=0
  • A [ i ] [ j ] = { 1 , 若 ( v i , v j ) 或 ⟨ v i , v j ⟩ 是 E ( G ) 中 的 边 0 , 若 ( v i , v j ) 或 ⟨ v i , v j ⟩ 不 是 E ( G ) 中 的 边 A[i][j] = \begin{cases} 1,若(v_i,v_j)或\left \langle v_i,v_j \right \rangle是E(G)中的边 \\ 0,若(v_i,v_j)或\left \langle v_i,v_j \right \rangle不是E(G)中的边\end{cases} A[i][j]={1(vi,vj)vi,vjE(G)0(vi,vj)vi,vjE(G)
  • 对于带权图而言,若顶点 v i v_i vi v j v_j vj 之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点 v i v_i vi v j v_j vj 不相连,则用 ∞ \infty 来代表这两个顶点之间不存在边
  • A [ i ] [ j ] = { w i j , 若 ( v i , v j ) 或 ⟨ v i , v j ⟩ 是 E ( G ) 中 的 边 0 或 ∞ , 若 ( v i , v j ) 或 ⟨ v i , v j ⟩ 不 是 E ( G ) 中 的 边 A[i][j] = \begin{cases} w_{ij},若(v_i,v_j)或\left \langle v_i,v_j \right \rangle是E(G)中的边 \\ 0或\infty,若(v_i,v_j)或\left \langle v_i,v_j \right \rangle不是E(G)中的边\end{cases} A[i][j]={wij(vi,vj)vi,vjE(G)0(vi,vj)vi,vjE(G)
    邻接矩阵法
#define MaxVertexNum 100                            //顶点数目的最大值
typedef char VertexType;                            //顶点的数据类型
typedef int EdgeType;                               //带权图中边上权值的数据类型
typedef struct {
	VertexType Vex[MaxVertexNum];                   //顶点表
	EdgeType Edge[MaxVertexNum][MaxVertexNum];      //邻接矩阵,边表
	int vexnum, arcnum;                             //图的当前顶点数和弧数
}MGraph
  • 注意
    • ① 在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)
    • ② 当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义为值为 0 和 1 的枚举类型
    • ③ 无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储
    • ④ 邻接矩阵表示法的空间复杂度为 O ( n 2 ) O(n^2) O(n2),其中 n n n 为图的顶点数 ∣ V ∣ |V| V
  • 特点
    • 无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素
    • ② 对于无向图,邻接矩阵的第 i i i 行(或第 i i i 列)非零元素(或非 ∞ \infty 元素)的个数正好是第 i i i 个顶点的 T D ( v i ) TD(v_i) TD(vi)
    • ③ 对于有向图,邻接矩阵的第 i i i 行(或第 i i i 列)非零元素(或非 ∞ \infty 元素)的个数正好是第 i i i 个顶点的出度 O D ( v i ) OD(v_i) OD(vi) [ 或入度 I D ( v i ) ID(v_i) ID(vi) ]
    • ④ 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列队每个元素进行检测,所花费的时间代价很大
    • 稠密图适合使用邻接矩阵的存储表示
    • ⑥ 设图 G G G 的邻接矩阵为 A A A A n A^n An 的元素 A n [ i ] [ j ] A^n[i][j] An[i][j] 等于由顶点 i i i 到顶点 j j j 的长度为 n n n 的路径的数目

6.2.2 邻接表法

  • 当一个图为稀疏图时,使用邻接表法,大大减少了空间的浪费
  • 所谓邻接表,是指对图 G G G 的每个顶点 v i v_i vi 建立一个单链表,第 i i i 个单链表中的结点表示依附于顶点 v i v_i vi 的边(对于有向图则是以顶点 v i v_i vi 为尾的弧),这个单链表就称为顶点 v i v_i vi边表(对于有向图则称出边表)。边表的头指针的顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点
  • 顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc)构成,边表(邻接表)结点由邻接点域(adjvex)和指向下一条邻接边的指针(nextarc)构成
    顶点表和边表结点结构
    邻接表法
#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 {                   //邻接表
	AdjList vertices;          
	int vexnum, arcnum;            //图的定点数和弧数
}ALGraph;                          //ALGraph是以邻接表存储的图类型
  • 特点
    • ① 若 G G G 为无向图,则所需的存储空间 O ( ∣ V ∣ + 2 ∣ E ∣ ) O(|V|+2|E|) O(V+2E);若 G G G 为有向图,则所需的存储空间 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)。前者的倍数 2 是由于无向图中,每条边在邻接表中出现了两次
    • ② 对于稀疏图,采用邻接表表示将极大地节省存储空间
    • ③ 在邻接表中,给定一顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为 O ( n ) O(n) O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低
    • ④ 若 G G G 为无向图,则结点的度为该节点边表的长度;若 G G G 为有向图,则结点的出度为该节点边表的长度,但计算入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度
    • ⑤ 图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序

6.2.3 十字链表

  • 十字链表是有向图的一种链式存储结构。在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点
  • 弧结点中有 5 个域:尾域(tailvex)和头域(headvex)分别指示弧尾和弧头这两个顶点在图中的位置;链域 hlink 指向弧头相同的下一个弧;链域 tlink 指向弧尾相同的下一个弧;info 域指向该弧的相同信息。这样,弧头相同的弧就在同一个链表上,弧尾相同的弧也在同一个链表上
  • 顶点结点中有 3 个域:data 域存放定点相关的数据信息,如顶点名称;firstin 和 fistout 两个域分别指向以该顶点为弧头或弧尾的第一个弧结点
  • 顶点结点之间是顺序存储的
    十字链表的顶点表结点和边表结点
    十字链表
#define MaxVertexNum 100                //图中顶点数目的最大值
typedef struct ArcNode {                //边表结点
	int tailvex, headvex;               //尾域和头域
	sturct ArcNode *hlink, *tlink;      //入弧单链表和出弧单链表
	//InfoType info;
}ArcNode;
typedef struct VNode {                  //顶点表结点
	VertexType data;                    //数据域
	ArcNode *firstin, *firstout;        //第一个入弧单链表和第一个出弧单链表
}VNode;
typedef struct {                        //十字链表
	VNode xlist[MaxVertexNum];          //顶点表
	int vexnum, arcnum;                 //图的定点数和弧数
}GLGraph;
  • 在十字链表中,既容易找到 v i v_i vi 为尾的弧,又容易找到 v i v_i vi 为头的弧,因而容易求得顶点的出度和入度
  • 图的十字链表表示是不唯一的,但一个十字链表表示确定的一个图

6.2.4 邻接多重表

  • 邻接多重表是无向图的另一种链式存储结构
  • 边结点:mark 为标志域,可用以标记该条边是否被搜索过;ivex 和 jvex 为该边依附的两个顶点在图中的位置;ilink 指向下一条依附于顶点 ivex 的边;jlink 指向下一条依附于顶点 jvex 的边,info 为指向和边相关的各种信息的指针域
  • 顶点结点:data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边
  • 在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,因此每个边结点同时链接在两个链表中。对无向图而言,其邻接多重表和邻接表的差别仅在于,同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点
    邻接多重表的结点
    邻接多重表
#define MaxVertexNum 100                //图中顶点数目的最大值
typedef struct ArcNode {                //边结点
	int ivex, jvex;                     //两个端点
	struct ArcNode *ilink, *jlink;      //两个端点的链表
	//InfoType info;                    //信息
	//bool mark;                        //标记该条边是否被搜索过
}ArcNode;
typedef struct VNode {                  //顶点结点
	VertexType data;                    //该顶点的数据
	ArcNode *firstedge;                 //该顶点的第一条边
}VNode; 
typedef struct {                        //邻接多重表
	VNode adjmulist[MaxVertexNum];      //顶点表
	int vexnum, arcnum;                 //图的定点数和弧数
}AMLGraph;

6.2.5 图的基本操作

  • Adjacent(G, x, y):判断图 G G G 是否存在边 ⟨ v i , v j ⟩ \left \langle v_i,v_j \right \rangle vi,vj ( v i , v j ) (v_i, v_j) (vi,vj)
  • Neighbors(G, x):列出图 G G G 中与结点 x x x 邻接的边
  • InsertVertex(G, x):在图 G G G 中插入顶点 x x x
  • DeleteVertex(G, x):在图 G G G 中删除顶点 x x x
  • AddEdge(G, x, y):若有向边 ⟨ v i , v j ⟩ \left \langle v_i,v_j \right \rangle vi,vj 或 无向边 ( v i , v j ) (v_i, v_j) (vi,vj) 不存在,则向图 G G G 中添加该边
  • RemoveEdge(G, x, y):若有向边 ⟨ v i , v j ⟩ \left \langle v_i,v_j \right \rangle vi,vj 或 无向边 ( v i , v j ) (v_i, v_j) (vi,vj) 存在,则向图 G G G 中删除该边
  • FirstNeighbor(G, x):求图 G G G 中顶点 x x x 的第一个邻接点,若有则返回顶点号。若 x x x 没有邻接点或图中不存在 x x x,则返回 − 1 -1 1
  • NextNeighbor(G, x, y):假设图 G G G 中顶点 y y y 是顶点 x x x 的一个邻接点,返回除 y y y 外顶点 x x x 的下一个邻接点的顶点号,若 y y y x x x 最后一个邻接点,则返回 − 1 -1 1
  • Get_edge_value(G, x, y):获取图 G G G 中边 ⟨ v i , v j ⟩ \left \langle v_i,v_j \right \rangle vi,vj ( v i , v j ) (v_i, v_j) (vi,vj) 对应的权值
  • Set_edge_value(G, x, y, v):设置图 G G G 中边 ⟨ v i , v j ⟩ \left \langle v_i,v_j \right \rangle vi,vj ( v i , v j ) (v_i, v_j) (vi,vj) 对应的权值为 v v v
  • 此外还有图的遍历算法:按照某种方式访问图中的每个顶点且仅访问一次。图的遍历算法包括深度优先遍历和广度优先遍历,具体见下一节的内容

6.3 图的遍历

  • 图的遍历是指从图中的某一顶点出发,按照某种搜索方式沿着图中的边对图中的所有顶点访问一次且仅访问一次
  • 注意到树是以一种特殊的图,所以树的遍历实际上也可视为一种特殊的图的遍历
  • 为避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点,为此可以设一个辅助数组vitis[]来标记顶点是否被访问过
  • 图的遍历算法主要有两种:广度优先搜索深度优先搜索

6.3.1 广度优先搜索(BFS)

  • 广度优先搜索(Breadth-First-Search, BFS)类似于二叉树的层次遍历
  • 基本思想是:
    • 首先访问起始顶点 v v v
    • 接着由 v v v 出发,依次访问 v v v 的的各个未被访问过的邻接顶点 w 1 , w 2 , . . . , w i w_1,w_2,...,w_i w1,w2,...,wi
    • 然后依次访问 w 1 , w 2 , . . . , w i w_1,w_2,...,w_i w1,w2,...,wi 的所有未被访问过的邻接顶点
    • 再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点,直至图中所有顶点都被访问过为止
    • 若果此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为始点,重复上述过程,直至图中所有顶点都被访问到为止
  • 为了实现逐层访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点
bool visited[MAX_VERTEX_NUM];                //访问标记数组
void BFSTraverse(Graph G) {                  //对图G进行广度优先遍历
	for(int i = 0; i < G.vexnum; i++) {
		visited[i] = false;                  //访问标记数组初始化
	}//for
	InitQueue(Q);                            //初始化辅助队列Q
	for(int i = 0; i < G.vexnum; i++) {      //从0号顶点开始遍历
		if(!visited[i]) {                    //对每个连通分量调用一次BFS
			BFS(G, i);                       //vi未被访问过,从vi开始BFS
		}//if
	}//for
}
void BFS(Graph G, int v) {                   //从顶点v出发,广度优先遍历图G
	visit(v);                                //访问初始顶点v
	visited[v] = true;                       //对v左已访问标记
	EnQueue(Q, v);
	while(!isEmpty(Q)) {
		DeQueue(Q, v);                       //顶点v出队列
		for(int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)) {
		                                     //检测v所有邻接点
			if(!visited[w]) {                //w为v的尚未访问的邻接顶点
				visit(w);                    //访问顶点w
				visited[w] = true;           //对w做已访问标记
				EnQueue(Q, w);               //顶点w如队列
			}//if
		}//for
	}//while
}
BFS算法的性能分析
  • 无论邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列 Q Q Q n n n 个顶点均需入队一次,在最坏的情况下,空间复杂度 O ( ∣ V ∣ ) O(|V|) O(V)
  • 采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次),故时间复杂度为 O ( ∣ V ∣ ) O(|V|) O(V),在搜索任意顶点的邻接点时,每条边至少访问一次,故时间复杂度为 O ( ∣ E ∣ ) O(|E|) O(E),故算法总的时间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为 O ( ∣ V ∣ ) O(|V|) O(V),故算法总的时间复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)
BFS算法求解单源最短路径问题
  • 若图 G = ( V , E ) G=(V,E) G=(V,E) 为非带权图,定义从顶点 u u u 到 顶点 v v v最短路径 d ( u , v ) d(u,v) d(u,v) 为从 u u u v v v 的任何路径中最少的边数;若从 u u u v v v 没有通路,则 d ( u , v ) = ∞ d(u,v) = \infty d(u,v)=
void BFS_MIN_Distance(Graph G, int u) {
	for(int i = 0; i < G.vexnum; i++) {
		d[i] = MAX;                     //初始化路径长度
	}
	visited[u] = true;
	d[u] = 0;
	EnQueue(Q, u);
	while(!isEmpty(Q)) {                //BFS算法主过程
		DeQueue(Q, u);                  //队头元素u出队
		for(int w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w)) {
			if(!visit[w]) {             //w为u的尚未访问的邻接顶点
				visited[w] = true;      //设已访问标记
				d[w] = d[u] + 1;        //路径长度加1
				EnQueue(Q, w);          //顶点w入队
			}//if
		}//while
	}
}
广度优先生成树
  • 在广度遍历的过程中,我们可以低到一棵遍历树,称为广度优先生成树
  • 一给定的图的邻接矩阵存储表示是唯一的,故其广度优先生成树也是唯一的,但由于邻接表存储表示不唯一,故其广度优先生成树也是不唯一
    图的广度优先生成树

6.3.2 深度优先搜索(DFS)

  • 深度优先搜索(Depth-Firse-Search, DFS)类似于树的先序遍历
  • 基本思想如下:
    • 首先访问图中某一起始顶点 v v v
    • 然后由 v v v 出发,访问与 v v v 邻接且未被访问的任一顶点 w 1 w_1 w1
    • 再访问与 w 1 w_1 w1邻接且未被访问的任一顶点 w 2 w_2 w2
    • 重复上述过程
    • 当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该顶点开始继续上述搜索过程,直至图中所有顶点均被访问过为止
bool visited[MAX_VERTEX_NUM];                //访问标记数组
void DFSTraverse(Graph G) {                  //对图G进行深度优先遍历
	for(int v = 0; v < G.vexnum; v++) {
		visited[v] = false;                  //初始化已访问标记数组
	}
	for(int v = 0; v < G.vexnum; v++) {      //本代码中是从v=0开始遍历
		if(!visited[v]) {
			DFS(G ,v);
		}
	}
}
void DFS(Graph G, int v) {                   //从顶点v出发,深度优先遍历图G
	visit(v);
	visited[v] = TRUE;                       //访问顶点v
	for(w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G ,v, w)) {
		if(!visited[w]) {                    //w为u的尚未访问的邻接顶点
			DFS(G, w);
		}
	}
}
  • 对于同样一个图,基于邻接矩阵的遍历所得到的DFS序列和BFS序列是唯一的;基于邻接表的遍历所得到的DFS序列和BFS序列是不唯一
DFS算法的性能分析
  • DFS算法是一个递归算法,需要借助一个递归工作栈,故其空间复杂度 O ( ∣ V ∣ ) O(|V|) O(V)
  • 邻接矩阵表示时,查找每个顶点的邻接点所需的时间为 O ( ∣ V ∣ ) O(|V|) O(V),故总的时间复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)
  • 邻接表表示时,查找所有顶点的邻接点所需的时间为 O ( ∣ E ∣ ) O(|E|) O(E),访问顶点所需的时间为 O ( ∣ V ∣ ) O(|V|) O(V),此时,总的时间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
深度优先的生成树和生成森林
  • 在深度遍历过程中,我们可以得到一颗遍历树,称为深度优先生成树(生成森林)
  • 对连通图调用DFS产生深度优先生成树,否则产生的是深度优先生成森林
  • 基于邻接表存储的深度优先生成树是不唯一的,基于邻接矩阵存储的深度优先生成树是唯一
    图的深度优先生成树

6.3.3 图的遍历与图的连通性

  • 图的遍历算法可以用来判断图的连通性
  • 对于无向图来说,若无向图是连通的,则从任一节点出发,仅需一次遍历就能够访问图中的所有顶点;若无向图是非连通的,则从某一顶点出发,一次遍历只能访问到该顶点所在连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问
  • 对于有向图来说,若从初始点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,否则不能访问到所有顶点
  • 对于无向图而言,在BFSTraverse()DFSTraverse()中,调用BFS(G, i)DFS(G,i)的次数等于该图的连通分量数
  • 对于有向图则不是这样,因为一个连通的有向图分为强联通的和非强联通的,它的连通子图也分为强连通分量和非强连通分量,非强连通分量一次低调用BFS(G, i)DFS(G,i)无法访问到该连通分量的所有顶点

6.4 图的应用

6.4.1 最小生成树

  • 生成树:连通图包含全部顶点的一个极小连通子图
  • 最小生成树:对于一个带权的连通无向图 G = ( V , E ) G=(V,E) G=(V,E) G G G 的所有生成树中边的权值之和最小的那颗生成树,称为 G G G最小生成树(Minimum-Spanning-Tree, MST)
  • 性质
    • 最小生成树不一定唯一的,即最小生成树的树形不一定唯一。当图 G G G 中的各边权值互不相等时, G G G 的最小生成树是唯一的;若无向连通图 G G G边数比顶点数少1,即 G G G 本身是一棵树时,则 G G G 的最小生成树就是它本身,此时,也是唯一
    • 最小生成树的边的权值之和是唯一的,虽然最小生成树不一定唯一,但其对应的边的权值之和总是唯一的,而且是最小的
    • 最小生成树的边数为顶点数减1
  • 最小生成树算法:贪心算法的策略(主要有Prim算法和Kruskal算法)
GENERIC_MST(G) {
	T = NULL;
	while T未形成一颗生成树;
		do 找到一条最小代价边(u,v)并且加入T后不会产生回路;
			T = T∪(u,v);
}
Prim算法
  • Prim(普里姆)算法的执行非常类似于寻找图的最短路径的Dijkstra算法
  • Prim算法构造最小生成树的过程
    • 初始时,从图 G G G 中任取一顶点加入树 T T T,此时树中只含有一个顶点
    • 之后,从图 G G G 中选择一个与当前 T T T 中顶点集合距离最近的顶点,并将该顶点和相应的边加入 T T T,每次操作后 T T T 中的顶点数和边数都增加 1 1 1
    • 以此类推,直至图中所有的顶点都并入 T T T,得到的 T T T 就是最小生成树,此时 T T T 中必然有 n − 1 n-1 n1 条边
  • Prim算法的步骤
    • 假设 G = { V , E } G = \{ V,E \} G={V,E} 是连通图,其最小生成树 T = ( U , E T ) T=(U,E_T) T=(U,ET) E T E_T ET 是最小生成树中边的集合
    • 初始化:向空树 T = ( U , E T ) T=(U,E_T) T=(U,ET) 中添加图 G = ( V , E ) G=(V,E) G=(V,E) 中的任一顶点 u 0 u_0 u0,使 U = { u 0 } U =\{u_0\} U={u0} E T = ∅ E_T=\varnothing ET=
    • 循环(重复下列操作直至 U = V U=V U=V):从图 G G G 中选择满足 { ( u , v ) ∣ u ∈ U , v ∈ V − U } \{ (u,v) | u \in U, v \in V-U \} {(u,v)uU,vVU} 且具有最小权值的边 ( u , v ) (u,v) (u,v),加入树 T T T,置 U = U ∪ { v } U = U \cup \{v\} U=U{v} E T = E T ∪ { ( u , v ) } E_T = E_T \cup \{(u,v)\} ET=ET{(u,v)}
void Prim(G, T){
	T =;                     //初始化空树
	U = {w};                   //添加任一顶点w
	while((V - U) !=) {      //若树中不含全部顶点(u,v)是使 u∈U与v∈(V-U),且权值最小的边;
		T = T∪{(u,v)};        //边归入树
		U = U∪{v};            //顶点归入树
	}
}
  • 需要辅助数组 min_weight[n]adjvex[n]
    • min_weight[n] 表示从当前树 T T T 中的顶点到下标为n的顶点的最小权值,0表示已加入树 T T T
    • adjvex[n] 表示下标为n的顶点是由下标为adjvex[n]的顶点达到的
      Prim算法
void MST_Prim(Graph G) {
	int min_weight[G.vexnum];
	int adjvex[G.vexnum];
	for(int i = 0; i < G.vexnum; i++) {  //初始化
		min_weight[i] = G.Edge[0][i];
		adjvex[i] = 0;
	}
	
	int min_arc;
	int min_vex;
	for(int i = 1; i < G.vexnum; i++) {  //树中顶点数=G.vexnum,第一个顶点已加入,因此循环G.vexnum-1遍
		min_arc = MAX;
		for(int j = 1; j < G.vexnum; j++) {
			if(min_weight[j] != 0 && min_weight[j] < min_arc) {  //查找权值最小的边及其连接的顶点
				min_arc = min_weight[j];
				min_vex = j;
			}
		}
		min_weight[min_vex] = 0;  //把该顶点加入树中
		for(int j = 0; j < G.vexnum; j++) {
			if(min_weight[j] != 0 && G.Edge[min_vex][j] < min_weight[j]) {  //对于未加入树中的顶点
				min_wight[j] = G.Edge[min_vex][j];  //更新其到树中新加入的顶点的边的最小权值
				adjvex[j] = min_vex;  //更新其当前最小权重的边所达到的顶点
			}
		}
	}
}
  • Prim算法的时间复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2),不依赖于 ∣ E ∣ |E| E,因此它适用于求解边稠密的图的最小生成树
Kruskal算法
  • Kruskal(克鲁斯卡尔)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法
  • Kruskal算法构造最小生成树的过程
    • 初始时,为只有 n n n 个顶点而无边的非连通图 T = { V , { } } T = \{ V, \{\}\} T={V,{}},每个顶点自成一个连通分量
    • 然后,按照边的权值由大到小的顺序,不断选取当前未被选取过且权值最小的边,若该边衣服的顶点落在 T T T 中不同的连通分量上,则将此边加入 T T T,否则舍弃此边而选择下一条权值最小的边
    • 以此类推,直至 T T T 中所有顶点都在一个连通分量上
  • Kruskal算法的步骤
    • 假设 G = ( V , E ) G = ( V,E ) G=(V,E) 是连通图,其最小生成树 T = ( U , E T ) T=(U,E_T) T=(U,ET)
    • 初始化: U = V U =V U=V E T = ∅ E_T=\varnothing ET=。即每个顶点构成一棵独立的树, T T T 此时是一个仅含 ∣ V ∣ |V| V 个顶点的森林
    • 循环(重复下列操作直至 T T T 是一棵树):按 G G G 的边的权值递增顺序依次从 E − E T E-E_T EET中选择一条边,若这条边加入 T T T 后不构成回路,则将其加入 E T E_T ET,否则舍弃,直到 E T E_T ET 中含有 n − 1 n-1 n1条边
void Kruskal(V,T) {
	T = V;                       //初始化树T,仅含顶点
	numS = n;                    //连通分量数
	while(numS > 1) {            //若连通分量数大于1
		从E中取出权值最小的边(u,v);
		if(v和u属于T中不同的连通分量) {
			T = T∪{(v,u)};      //将此边加入生成树中
			numS--;              //连通分量数减1
		}
	}
}
  • 需要借助堆排序并查集
    • 采用堆排序来存放边的集合
    • 由于生成树 T T T 中的所有边可视为一个等价类,因此每次添加新的边的过程类似于求解等价类的过程,由此可以采用并查集的数据结构来描述 T T T
typedef struct Edge {
	int a, b;        //边对应两个端点的下标
	int weight;      //边的权值
};
void MST_Kruskal(Graph G, Edge* edges, int *parent) {
	heap_sort(edges);                             //对边进行堆排序
	Initial(parent);                              //初始化并查集
	for(int i = 0; i < G.arcnum; i++) {
		int a_root = Find(parent, edges[i].a);    //查找顶点a在并查集中的根结点
		int b_root = Find(parent, edges[i].b);    //查找顶点a在并查集中的根结点
		if(a_root != b_root) {                    //如果顶点a和顶点b不在同一个集合(连通分量)中
			Union(parent, a_root, b_root);        //连接两个顶点,合并两个连通分量
		}
	}
}
  • Kruskal算法的时间复杂度 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(ElogE) (堆排序 O ( l o g ∣ E ∣ ) O(log|E|) O(logE)),因此Kruskal算法适适合于边稀疏而顶点较多的图

6.4.2 最短路径

  • 当图时带权图时,把从一个顶点 V 0 V_0 V0 到图中其余任意一个顶点 v i v_i vi 的一条路径(可能不止一条)所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的那条路径称为最短路径
  • 求解最短路径的算法通常都依赖于一种性质,即两点之间的最短路径也包含了路径上其他顶点间的最短路径。带权有向图 G G G 的最短路径问题一般可分为两类:一是单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可通过Dijkstra(迪杰斯特拉)算法求解;二是求没对顶点间的最短路径,可通过Floyd(弗洛伊德)算法来求解
Dijkstra算法求单源最短路径问题
  • Dijkstra算法设置一个集合 S S S 记录已求得的最短路径的顶点,初始时把源点 v 0 v_0 v0 放入 S S S,集合 S S S 每并入一个新顶点 v i v_i vi,都要修改源点 v 0 v_0 v0 到集合 V − S V-S VS 中顶点当前的最短路径长度值
  • 还设置了两个辅助数组
    • dist[]:记录从源点 v 0 v_0 v0 到其他各顶点当前的最短路径长度,它的初态为:若从 v 0 v_0 v0 v i v_i vi 有弧,则dist[i]为弧上的权值;否则置dist[i] ∞ \infty
    • path[]path[i]表示从源点到顶点i之间的最短路径的前驱结点。在算法结束时,可根据其值追溯得到源点 v 0 v_0 v0 到顶点 v i v_i vi 的最短路径
  • 假设从顶点 0 0 0 出发,即 v 0 = 0 v_0 = 0 v0=0,集合 S S S 最初只包含顶点 0 0 0,邻接矩阵 a r c s arcs arcs 表示带权有向图,Dijkstra 算法的步骤如下(不考虑对 path[] 的操作)
    • 1)初始化:集合 S S S 初始为 { 0 } \{0\} {0} d i s t [ ] dist[] dist[] 的初始值 d i s t [ i ] = a r c s [ 0 ] [ i ] , i = 1 , 2 , . . . , n − 1 dist[i]=arcs[0][i], i = 1,2,...,n-1 dist[i]=arcs[0][i],i=1,2,...,n1
    • 2)从顶点集合 V − S V-S VS 中选出 v j v_j vj,满足 d i s t [ j ] = M i n { d i s t [ i ] ∣ v i ∈ V − S } dist[j] = Min\{dist[i] | v_i \in V-S\} dist[j]=Min{dist[i]viVS} v j v_j vj 就是当前求得的一条从 v 0 v_0 v0 出发的最短路径的终点,令 S = S ∩ { j } S = S \cap \{j\} S=S{j}
    • 3)修改从 v 0 v_0 v0 出发到集合 V − S V-S VS 上任一顶点 v k v_k vk 可达的最短路径长度:若 d i s [ j ] + a r c s [ j ] [ k ] < d i s t [ k ] dis[j] + arcs[j][k] < dist[k] dis[j]+arcs[j][k]<dist[k],则更新 d i s t [ k ] = d i s t [ j ] + a r c s [ j ] [ k ] dist[k] = dist[j] + arcs[j][k] dist[k]=dist[j]+arcs[j][k]
    • 4)重复 2)~3)操作共 n − 1 n-1 n1 次,直到所有的顶点都包含在 S S S
      Dijkstra算法
      Dijkstra算法path[]数组
void Dijkstra(Graph G, int v) {  //v为起始顶点
	int s[G.vexnum];
	int path[G.vexnum];
	int dist[G.vexnum];
	for(int i = 0; i < G.vexnum; i++) {  //初始化,上述步骤第1)步
		dist[i] = G.edge[v][i];
		s[i] = 0;
		if(G.edge[v][i] < MAX)
			path[i] = v;
		else
			path[i] = -1;
	}
	s[v] = 1;
	path[v] = -1;

	for(int i = 0; i < G.vexnum; i++) {
		int min = MAX;
		int u;
		for(int j = 0; j < G.vexnum; j++) {  //上述步骤第2)步
			if(s[j] == 0 && dist[j] < min) {
				min = dist[j];
				u = j;
			}
		}
		s[u] = 1;
		for(int j = 0; j < G.vexnum; j++) {  //上述步骤第3)步
			if(s[j] == 0 && dist[u] + G.edge[u][j] < dist[j]) {
				dist[j] = dist[u] + G.edges[u][i];
				path[j] = u;
			}
		}
	}
}
  • 时间复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)
  • 边上有负权值时,Dijkstra算法并不适用
Floyd算法求各顶点之间最短路径问题
  • Floyd算法的基本思想是:
    • 递推产生一个 n n n 阶方阵序列 A ( − 1 ) , A ( 0 ) , A ( 1 ) , . . . , A ( k ) , . . . , A ( n − 1 ) A^{(-1)}, A^{(0)}, A^{(1)}, ..., A^{(k)}, ..., A^{(n-1)} A(1),A(0),A(1),...,A(k),...,A(n1),其中 A ( k ) [ i ] [ j ] A^{(k)}[i][j] A(k)[i][j] 表示从顶点 v i v_i vi 到顶点 v j v_j vj 的路径长度, k k k 表示绕行第 k k k 个顶点的运算步骤
    • 初始时,对于任意两个顶点 v i v_i vi v j v_j vj,若它们之间存在边,则以此边上的权值作为它们之间的最短路径长度;若它们之间不存在边,则以 ∞ \infty 作为它们的最短路径长度
    • 以后逐步尝试在原路径中加入顶点 k ( k = 0 , 1 , . . . , n − 1 ) k(k=0,1,...,n-1) k(k=0,1,...,n1)作为中间顶点。若增加中间顶点后,得到 的路径比原来的路径长度减少了,则以此新路径代替原路径
  • 算法描述如下:
    • 定义一个 n n n 阶方阵序列 A ( − 1 ) , A ( 0 ) , A ( 1 ) , . . . , A ( n − 1 ) A^{(-1)}, A^{(0)}, A^{(1)}, ..., A^{(n-1)} A(1),A(0),A(1),...,A(n1),其中
    • A ( − 1 ) [ i ] [ j ] = a r c [ i ] [ j ] A^{(-1)}[i][j] = arc[i][j] A(1)[i][j]=arc[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] = 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]=Min{A(k1)[i][j],A(k1)[i][k]+A(k1)[k][j]},k=0,1,...,n1
    • 式中, A ( 0 ) [ i ] [ j ] A^{(0)}[i][j] A(0)[i][j] 是从顶点 v i v_i vi v j v_j vj、中间顶点是 v 0 v_0 v0 的最短路径长度, A ( k ) [ i ] [ j ] A^{(k)}[i][j] A(k)[i][j] 是从顶点 v i v_i vi v j v_j vj 、中间顶点的序号不大于 k k k 的最短路径长度
  • Floyd算法是一个迭代的过程,每迭代一次,在从 v i v_i vi v j v_j vj 的最短路径上就多考虑了一个顶点;经过 n n n 次迭代后,所得到的 A ( n − 1 ) [ i ] [ j ] A^{(n-1)}[i][j] A(n1)[i][j] 就是 v i v_i vi v j v_j vj 的最短路径长度,即方阵 A ( n − 1 ) A^{(n-1)} A(n1) 就保存了任意一对顶点之间的最短路径长度
    Floyd算法
void Floyd(Graph G) {
	int A[G.vexnum][G.vexnum];
	for(int i = 0; i < G.vexnum; i++) {  //初始化
		for(int j = 0; j < G.vexnum; j++) {
			A[i][j] = G.Edge[i][j];
		}//for j
	}//for i
	for(int k = 0; k < G.vexnum; k++) {  //迭代
		for(int i = 0; i < G.vexnum; i++) {
			for(int j = 0; j < G.vexnum; j++) {
				if(A[i][j] > A[i][k] + A[k][j]) {
					A[i][j] = A[i][k] + A[k][j];
				}//if
			}//for j
		}//for i
	}//for k
}
  • Floyd算法时间复杂度 O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)
  • Floyd算法允许图中有带负权值的边,但不允许包含带负权值的边组成的回路
  • Floyd算法同样适用于带权无向图,因为带权无向图可视为权值相同往返二重边的有向图
  • 也可以用单源最短路径算法来解决每对顶点之间的最短路径问题。轮流将每个顶点作为源点,并且在所有边权值均非负时,运行一次Dijkstra算法,其时间复杂度为 O ( ∣ V ∣ 2 ) ⋅ ∣ V ∣ = O ( ∣ V ∣ 3 ) O(|V|^2) \cdot |V| = O(|V|^3) O(V2)V=O(V3)

6.4.3 有向无环图描述表达式

  • 有向无环图:若一个有向图中不存在环,则称为有向无环图,简称DAG图
  • 有向无环图是描述含有公共子式的表达式的有效工具,在二叉树中,有的子式会重复出现,若利用有向无环图,则可实现对相同子式的共享,从而节省存储空间

6.4.4 拓扑排序(AOV网)

  • AOV 网:若用DAG网表示一个工程,其顶点表示活动,用有向边 ⟨ V i , V j ⟩ \left \langle V_i, V_j \right \rangle Vi,Vj 表示活动 V i V_i Vi 必须先于活动 V j V_j Vj 进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,记为AOV 网
  • 在 AOV 网中,活动 V i V_i Vi 是活动 V j V_j Vj 的直接前驱,活动 V j V_j Vj 是活动 V i V_i Vi 的直接后继,这种前驱和后继的关系具有传递性,且任何活动 V i V_i Vi 不能以它自己作为自己的前驱或后继
  • 拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
    • ① 每个顶点出现且只出现一次
    • ② 若顶点 A 在序列中排在顶点 B 的前面,则图中不存在从顶点 B 到顶点 A 的路径
    • 或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点 A 到顶点 B 的路径,则在排序用顶点 B 出现在顶点 A 的后面
    • 每个 AOV 网都有一个或多个拓扑排序序列
  • 算法思想
    • ① 从 AOV 网中选择一个没有前驱(入度为0)的顶点并输出
    • ② 从网中删除该顶点和所有以它为起点的有向边(出边)
    • ③ 重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。后一种切情况说明有向图中必然存在环
bool TopologicalSort(Graph G) {
	InitStack(S);                //初始化栈,存储入度为0的顶点
	for(int i = 0; i < G.vexnum; i++) {
		if(indegree[i] == 0)
			Push(S, i);          //将所有入度为0的顶点进栈
	}
	int count = 0;               //计数,记录当前已经输出的顶点数
	while(!isEmpty(S)) {         //栈不空,则存在入度为0的顶点
		Pop(S, i);               //栈顶元素出栈
		print[count++] = i;      //输出顶点i
		for(ArcNode* p = G.vertices[i].firstarc; p; p = p->nextarc) {
		//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈S
			VNode v = p->adjvex;
			if(!(--indegree[v]))
				Push(S, v);      //入度为0,则入栈
		}
	}//while
	if(count < G.vexnum)
		return false;            //排序失败,有向图中有回路
	else
		return true;		     //拓扑排序成功
}
  • 拓扑排序的时间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 利用深度优先遍历也可以实现拓扑排序
  • 对于一个 AOV 网,如果采用下列步骤进行排序,则称之为逆拓扑排序
    • ① 从 AOV 网中选择一个没有后继(出度为0)的顶点并输出
    • ② 从网中删除该顶点和所有以它为终点的有向边
    • ③ 重复①和②直到当前的 AOV 网为空
  • 用拓扑排序算法处理 AOV 网时,应注意以下问题:
    • ① 入度为零的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续
    • ② 若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一;但若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的
    • ③ 由于 AOV 网中各顶点的地位平等,每个顶点的编号都是人为的,因此可以按拓扑排序的结果重新编号,生成 AOV 网的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于一般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立

6.4.5 关键路径(AOE网)

  • 在带权有向图中,以顶点表示时间,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE 网
  • AOE 网具有以下两个性质
    • ① 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
    • ② 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生
  • 在 AOE 网中仅有一个入度为 0 的顶点,称为开始顶点(源点),它表示整个工程的开始;网中也仅存在一个出度为 0 的顶点,称为结束顶点(汇点),它表示整个工程的结束
  • 从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动关键活动。完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和
事件 v k v_k vk 的最早发生时间 v e ( k ) ve(k) ve(k)
  • 它是指从源点 v 1 v_1 v1 到顶点 v k v_k vk 的最长路径长度。事件 v k v_k vk 的最早发生时间决定了所有从 v k v_k vk 开始的活动能够开工的最早时间。可用下面的递推公式来计算:
    • v e ( 源 点 ) = 0 ve(源点) = 0 ve()=0
    • v e ( k ) = M a x { v e ( j ) + W e i g h t ( v j , v k ) } ve(k) = Max \{ve(j) + Weight(v_j, v_k)\} ve(k)=Max{ve(j)+Weight(vj,vk)} v k v_k vk v j v_j vj 的任意后继, W e i g h t ( v j , v k ) Weight(v_j, v_k) Weight(vj,vk) 表示 ⟨ v j , v k ⟩ \left \langle v_j, v_k \right \rangle vj,vk 上的权值
  • 计算 v e ( ) ve() ve() 值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算
    • ① 初始时,令 v e [ 1... n ] = 0 ve[1...n] = 0 ve[1...n]=0
    • ② 输出一个入度为 0 的顶点 v j v_j vj 时,计算它所有直接后继顶点 v k v_k vk 的最早发生时间, 若 v e [ j ] + W e i g h t ( v j , v k ) > v e [ k ] ve[j] + Weight(v_j, v_k) > ve[k] ve[j]+Weight(vj,vk)>ve[k],则 v e [ k ] = v e [ j ] + W e i g h t ( v j , v k ) ve[k] = ve[j] + Weight(v_j, v_k) ve[k]=ve[j]+Weight(vj,vk)。以此类推,直至输出全部顶点
事件 v k v_k vk 的最迟发生时间 v l ( k ) vl(k) vl(k)
  • 它是指在不推迟整个工程完成的前提下,即保证它的后续事件 v j v_j vj 在其最迟发生时间 v l ( j ) vl(j) vl(j) 能够发生时,改时间最迟必须发生的时间。可用下面的递推公式来计算:
    • v l ( 汇 点 ) = v e ( 汇 点 ) vl(汇点) = ve(汇点) vl()=ve()
    • v l ( k ) = M i n { v l ( j ) − W e i g h t ( v k , v j ) } vl(k) = Min \{ vl(j) - Weight(v_k, v_j) \} vl(k)=Min{vl(j)Weight(vk,vj)} v k v_k vk v j v_j vj 的任意前驱
  • 计算 v l ( ) vl() vl()值 时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算(增设一个栈以记录拓扑序列,拓扑排序结束后从栈顶至栈底便为逆拓扑有序序列):
    • ① 初始时,令 v l [ 1... n ] = v e [ n ] vl[1...n] = ve[n] vl[1...n]=ve[n]
    • ② 栈顶顶点 v j v_j vj 出栈,计算其所有直接前驱顶点 v k v_k vk 的最迟发生时间,若 v l [ j ] − W e i g h t ( v k , v j ) < v l [ k ] vl[j] - Weight(v_k, v_j) < vl[k] vl[j]Weight(vk,vj)<vl[k],则 v l [ k ] = v l [ j ] − W e i g h t ( v k , v j ) vl[k] = vl[j] - Weight(v_k, v_j) vl[k]=vl[j]Weight(vk,vj)。以此类推,直至输出全部栈中顶点
活动 a i a_i ai 的最早开始时间 e ( i ) e(i) e(i)
  • 它是指该活动弧的起点所表示的事件的最早发生时间
  • 若边 ⟨ v k , v j ⟩ \left \langle v_k, v_j \right \rangle vk,vj 表示活动 a i a_i ai,则有 e ( i ) = v e ( k ) e(i) = ve(k) e(i)=ve(k)
活动 a i a_i ai 的最迟开始时间 l ( i ) l(i) l(i)
  • 它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差
  • 若边 ⟨ v k v j ⟩ \left \langle v_k v_j \right \rangle vkvj 表示活动 a i a_i ai,则有 l ( i ) = v l ( j ) − W e i g h t ( v k , v j ) l(i) = vl(j) - Weight(v_k, v_j) l(i)=vl(j)Weight(vk,vj)
一个活动 a i a_i ai 的最迟开始时间 l ( i ) l(i) l(i) 和其最早开始时间 e ( i ) e(i) e(i) 的差额 d ( i ) = l ( i ) − e ( i ) d(i) = l(i) - e(i) d(i)=l(i)e(i)
  • 它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动 a i a_i ai 可以拖延的时间
  • 若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称 l ( i ) − e ( i ) = 0 l(i) - e(i) = 0 l(i)e(i)=0 l ( i ) = e ( i ) l(i) = e(i) l(i)=e(i) 的活动 a i a_i ai 是关键活动
求关键路径
  • 算法步骤如下:
    • 1)从源点出发,令 v e ( 源 点 ) = 0 ve(源点) = 0 ve()=0,按拓扑有序求其余顶点的最早发生时间 v e ( ) ve() ve()
    • 2)从汇点出发,令 v l ( 汇 点 ) = v e ( 汇 点 ) vl(汇点) = ve(汇点) vl()=ve(),按逆拓扑有序求其余顶点的最迟发生时间 v l ( ) vl() vl()
    • 3)根据各顶点的 v e ( ) ve() ve() 值求所有弧的最早开始时间 e ( ) e() e()
    • 4)根据各顶点的 v l ( ) vl() vl() 值求所有弧的最迟开始时间 l ( ) l() l()
    • 5)求 AOE 网中所有活动的差额 d ( ) d() d(),找出所有 d ( ) = 0 d()=0 d()=0 的活动构成关键路径
      关键路径
  • 注意以下几点:
    • 1)关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活动来缩短整个工程的工期。但也不能随意缩短关键活动,因为一旦缩短到一定程度,该关键活动就可能会变成非关键活动
    • 2)网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你好!关于学习数据结构的C语言笔记,我可以给你一些基本的指导和概念。数据结构是计算机科学中非常重要的一门课程,它涉及存储和组织数据的方法。C语言是一种常用的编程语言,很适合用于实现各种数据结构。 下面是一些数据结构的基本概念,你可以在学习笔记中包含它们: 1. 数组(Array):一种线性数据结构,可以存储相同类型的元素。在C语言中,数组是通过索引访问的。 2. 链表(Linked List):也是一种线性数据结构,但不需要连续的内存空间。链表由节点组成,每个节点包含数据和指向下一个节点的指针。 3. 栈(Stack):一种后进先出(LIFO)的数据结构,类似于装满物品的箱子。在C语言中,可以使用数组或链表来实现栈。 4. 队列(Queue):一种先进先出(FIFO)的数据结构,类似于排队等候的队伍。同样可以使用数组或链表来实现队列。 5. 树(Tree):一种非线性数据结构,由节点和边组成。每个节点可以有多个子节点。二叉树是一种特殊的树结构,每个节点最多有两个子节点。 6. (Graph):另一种非线性数据结构,由节点和边组成。可以用来表示各种实际问题,如社交网络和地。 这只是数据结构中的一些基本概念,还有其他更高级的数据结构,如堆、哈希表和二叉搜索树等。在学习笔记中,你可以介绍每个数据结构的定义、操作以及适合使用它们的场景。 希望这些信息对你有所帮助!如果你有任何进一步的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值