第六章:图
6.1 图的基本概念
-
有向图:若E是有向边(也称弧)的有限集合时,则图G为有向图 。弧记 <v,w> ,v为弧尾,w为弧头,也称从 v 到 w 的弧
-
无向图:若E是无向边(简称边)的有限集合时,则图G为无向图。边记 (v,w) ,是无序的。
-
简单图、多重图:一个图如果满足(1)不存在重复边;(2)不存在顶点到自身的边,那么称图G为简单图。
-
完全图(也称为简单完全图):对于无向图, ∣ E ∣ |E| ∣E∣ 的取值范围为 0 0 0 到 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2,其中有 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2 条边的无向图称为完全图,在完全图中任意两个顶点之间都存在边。对于有向图, ∣ E ∣ |E| ∣E∣ 的取值范围为 0 0 0 到 n ( n − 1 ) n(n-1) n(n−1),其中有 n ( n − 1 ) n(n-1) n(n−1) 条弧的有向图称为有向完全图,在有向完全图中任意两个顶点之间都存在两条方向相反的弧。
-
子图:设有两个图 G=( V , E ) 和 G’ =( V’ , E’ ) ,若 V’ 是 V 的子集,且 E’ 是 E 的子集,则称 G’ 是 G 的子图。若有满足 V(G’)=V(G) 的子图 G’ ,则称 G’ 为 G 的生成子图。
并非 V 和 E 的任何子集都能构成 G 的子图,因为这样的子集很可能无法构成一个图,即 E 的子集中的某些边关联的顶点可能不在这个 V 的子集中。
- 连通、连通图和连通分量:在无向图中若,若从顶点 v 到顶点 w 有路径存在,则称 v 和 w 是连通的。若图 G 中任意两个顶点都是连通的,则称图 G 为连通图,否则称为非联通图。无向图中的极大连通子图称为连通分量。
如下图所示:
1)如果图有n个顶点,边数小于n-1,则该图必是非连通图
2)如果图有n个顶点,并且该图是非连通图,那么该图最多可以有几条边?
由 n-1 个顶点构成一个完全图,剩下的第 n 个顶点不与任何顶点相连,即可得到非连通图中边数最多的情况。
- 强连通图,强连通分量:在有向图中,如果有一对顶点 v 和 w ,从 v 到 w 和从 w 到 v 之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量。
如下图所示。
上图的强连通分量如下:
如果一个有向图有 n 个顶点,且该图为强连通图,那么最少需要多少条边?
至少需要 n 条边,构成一个环路
- 生成树,生成深林:连通图的生成树是一个极小连通子图。若连通图中顶点数为 n ,则它的生成树含有 n-1 条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成回路。
如下图所示:
上图的其中一个生成树如下:
区分极小连通子图和极大连通子图:
极小连通子图:是连通图的生成树,是一个图中,保持连通的前提下,还要让边数最少的子图。
极大连通子图:是无向图的连通分量,极大要求连通子图包含其所有的边,连通顶点的任何边都不能舍弃。
- 顶点的度,入度和出度:在无向图中,顶点 v 的度是指依附于顶点 v 的边的边数,极为 TD(v) 。在有向图中,顶点 v 的度分为入度和出度,入度是以顶点 v 为终点的有向边的数目,记为 ID(v) ;出度是以顶点 v 为起点的有向边的数目,记为OD(v),顶点 v 的度等于其入度和出度之和,即 TD(v)=ID(v)+OD(v)
无向图中所有顶点的度之和为边数的2倍
- 有向树:一个顶点的入度为0、其余顶点入度均为1的有向图,称为有向树。
- 回路(环):第一个顶点和最后一个顶点相同的路径称为回路或环。
若一个图有n个顶点,并且有大于n-1条边,则此图一定有环。
- 简单路径、简单回路:在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路叫简单回路。
- 稀疏图、稠密图:边数很少的图称为稀疏图,反之称为稠密图。
6.2 图的存储及基本操作
邻接矩阵法
用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。
结点数为 n 的图 G=(V,E) 的邻接矩阵 A 是 n*n 的:
A
[
i
]
[
j
]
=
{
1
,
若
(
v
i
,
v
j
)
是
E
(
G
)
中
的
边
0
,
若
(
v
i
,
v
j
)
不
是
E
(
G
)
中
的
边
A[i][j]= \begin{cases} 1,若(v_i,v_j)是E(G)中的边\\ 0,若(v_i,v_j)不是E(G)中的边 \end{cases}
A[i][j]={1,若(vi,vj)是E(G)中的边0,若(vi,vj)不是E(G)中的边
对带权图而言
A
[
i
]
[
j
]
=
{
w
i
j
,
若
(
v
i
,
v
j
)
是
E
(
G
)
中
的
边
∞
或
0
,
若
(
v
i
,
v
j
)
不
是
E
(
G
)
中
的
边
A[i][j]= \begin{cases} w_{ij},若(v_i,v_j)是E(G)中的边\\ \infty或0,若(v_i,v_j)不是E(G)中的边 \end{cases}
A[i][j]={wij,若(vi,vj)是E(G)中的边∞或0,若(vi,vj)不是E(G)中的边
代码定义如下:
(教材)
#define MAX_VERTEX_NUM 20 //图最大顶点个数
typedef enum{DG,DN,UDG,UDN} GraphKind;//{有向图,有向网,无向图,无向网}
//enum是枚举数据类型,相当于#define DG 0,#define DN 1...
//第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1
typedef int VRType;
typedef char VertexType;//图的结点数据类型
typedef string InfoType;
typedef struct ArcCell{
VRType adj;//VRTpye是顶点关系类型,对无权图,用1和0表示相邻否;对带权图,则为权值类型
InfoType *info;//该弧相关的提示信息
}ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef struct {
VertexType vexs[MAX_VERTEX_NUM];//顶点向量
AdjMatrix arcs;//图的邻接矩阵
int vexnum,arcnum;//图的当前顶点数和弧数
GraphKind kind;//图的种类标志
} MGraph;
(考试简化)
#define MAX_VERTEX_NUM 20 //图最大顶点个数
typedef char VertexType;//图的结点数据类型
typedef int EdgeType;
typedef struct {
VertexType vexs[MAX_VERTEX_NUM];//顶点向量
EdgeType edges[MAX_VERTEX_NUM][MAX_VERTEX_NUM];//图的邻接矩阵
int vexnum,arcnum;//图的当前顶点数和弧数
} MGraph;
notes:
1)无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可以采用压缩存储、
2)邻接矩阵表示法的空间复杂度为 O ( n 2 ) O(n^2) O(n2),其中 n 为图的顶点数。
3)稠密图适合使用邻接矩阵存储表示
4)设图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 的路径的数目
邻接表法
当一个图为稀疏图时,用邻接矩阵显然会浪费大量空间,邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
对无向图 G 中每个顶点
v
i
v_i
vi 建立一个单链表,第 i 个单链表中的结点表示依附于顶点
v
i
v_i
vi 的边,这个单链表就称为顶点
v
i
v_i
vi 的边表。
对有向图 G 中每个顶点
v
i
v_i
vi 建立一个单链表,第 i 个单链表中的结点表示以顶点
v
i
v_i
vi 为尾的弧,这个单链表就称为顶点
v
i
v_i
vi 的出边表。
代码定义如下:
#define MAX_VERTEX_NUM 20 //图最大顶点个数
typedef char VertexType;//图的结点数据类型
typedef int InfoType;
typedef struct ArcNode{//边表结点
int adjvex;//该弧所指向的顶点的位置
struct ArcNode *next;//指向下一条弧的指针
//如果为网,则还要添加弧的权值信息
InfoType info;
}ArcNode;
typedef struct VNode{//顶点表的信息
VertexType data;//顶点信息
ArcNode *first;//指向第一条依附该顶点的弧的指针
}VNode,AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices;//邻接表
int vexnum,arcnum;//图的顶点数和弧数
}ALGraph;
notes:
1)如果 G 为无向图,则所需的存储空间为 O ( ∣ V ∣ + 2 ∣ E ∣ ) O(|V|+2|E|) O(∣V∣+2∣E∣);若G为有向图,则所需的存储空间为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
2)对于稀疏图,采用邻接表表示将极大的节省存储空间
3)在有向图的邻接表表示中,求一个给定顶点的出度,计算其邻接表中的结点个数即可,但求其顶点的入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。
4)图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法和边的输入次序。
十字链表
在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点。
弧结点中有5个域:尾域(tailvex)和头域(headvex)分别指向弧尾和弧头这两个顶点在顶点结点表中的位置,链域 hlink 指向弧头相同的下一条弧,链域 tlink 指向弧尾相同的下一条弧,info 用来存放弧权值。
顶点结点中有 3 个域:data 域存放顶点相关的数据信息,如顶点名称;firstin 和 firstout 两个域分别指向以顶点为弧头或弧尾的第一个弧结点
图的十字链表表示也不是唯一的,但是一个十字链表表示且确定一个图。
邻接多重表
邻接多重表示无向图的另一种链式存储结构
与十字链表类似,在邻接多重表中,每条边用一个结点表示,其结构如下所示
mark 为标志域,可用以标记该条边是否被搜索过;ivex 和 jvex 为该边依附的两个顶点在顶点表中的位置;ilink 指向下一条依附于顶点 ivex 的边;jlink 指向下一条依附于顶点 jvex 的边,info为边权值
每个顶点组成顶点表,表结点由如下所示的两个域组成:
data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。
6.3 图的遍历
从图的某一顶点出发,按某种搜索方法沿着图的边对图中的所有顶点访问一次
广度优先遍历
类似于二叉树的层次遍历。基本思想如下:
1)访问起始结点 v;
2)接着从 v 出发,依次访问 v 的各个未访问过的邻接顶点
w
1
,
w
2
,
.
.
.
,
w
i
w_1,w_2,...,w_i
w1,w2,...,wi存入队列中,并标记访问过得结点;
3)若队列不为空,则将出队结点
w
i
w_i
wi视为起始结点v,从第一步再开始遍历;
4)若队列为空,且还有未访问结点,则选一个未访问节点来作为起始点,从第一步开始遍历,直到所有顶点都被访问为止
基于邻接矩阵表示的BFS代码实现如下:
bool visited[MAX_VERTEX_NUM];//访问标记数组
void BFS(MGraph G,int i){
cout<<G.vexs[i];//访问初始顶点;
visited[i]=TRUE;//标记访问点
//初始化辅助队列
int queue[MAX_VERTEX_NUM];//辅助队列
int qhead=0,qtail=0;//队头,队尾指针
queue[qtail++]=i;//初始访问点入队
while(qtail!=qhead){//队列中还有待遍历访问点
i=queue[qhead++];//将队列中的队头结点,当做新的初始结点进行遍历访问,并出队
for (int j=0; j<G.vexnum; j++) {
if(G.edges[i][j]!=INFINITY&&i!=j){//检查初始结点的所有邻接点
if(!visited[j]){//该邻接点尚未访问过
cout<<G.vexs[j];//访问结点并显示
visited[j]=TRUE;//标记访问点
queue[qtail++]=j;//入队
}
}
}
}
}
void BFSTracerse(MGraph G){
memset(visited, FALSE, sizeof(visited));//初始化数组
for (int i=0; i<G.vexnum; i++) {//从0号结点开始遍历
if (!visited[i]) {//该结点未被访问
BFS(G,i);//将该结点作为新一轮遍历的初始结点i进行遍历
}
}
cout<<endl;
}
基于邻接表表示的BFS代码如下:
bool visited[MAX_VERTEX_NUM];//访问标记数组
void BFS(ALGraph G,int i){
cout<<G.vertices[i].data;//访问初始顶点;
visited[i]=TRUE;//标记访问点
//初始化辅助队列
int queue[MAX_VERTEX_NUM];//辅助队列
int qhead=0,qtail=0;//队头,队尾指针
queue[qtail++]=i;//初始访问点入队
while(qtail!=qhead){//队列中还有待遍历访问点
i=queue[qhead++];//将队列中的队头结点,当做新的初始结点进行遍历访问,并出队
for (ArcNode *p=G.vertices[i].first; p!=NULL ; p=p->next) {//检查初始结点的所有邻接点
if(!visited[p->adjvex]){//该邻接点尚未访问过
cout<<G.vertices[p->adjvex].data;//访问结点并显示
visited[p->adjvex]=TRUE;//标记访问点
queue[qtail++]=p->adjvex;//入队
}
}
}
}
void BFSTracerse(ALGraph G){
memset(visited, FALSE, sizeof(visited));//初始化数组
for (int i=0; i<G.vexnum; i++) {//从0号结点开始遍历
if (!visited[i]) {//该结点未被访问
BFS(G,i);//将该结点作为新一轮遍历的初始结点i进行遍历
}
}
cout<<endl;
}
BFS算法性能分析:
1)空间复杂度:无论是邻接表还是邻接矩阵,最坏情况下都为 O ( ∣ V ∣ ) O(|V|) O(∣V∣)。消耗的空间主要是需要借助一个额外的辅助队列Q来实现遍历。
2)时间复杂度:
1、采用邻接表的图,遍历所有顶点的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣),而搜索任一顶点的邻接点,要查找所有的边,时间复杂度为 O ( ∣ E ∣ ) O(|E|) O(∣E∣);
2、采用邻接矩阵存储方式,遍历的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),而收缩任一顶点的邻接点所需时间为 O ( ∣ V ∣ ) O(|V|) O(∣V∣)。
BFS算法可以求解单源最短路径:
若图G为非带权图,定义从顶点a到顶点b的最短路径为从a到b的任何路径中最少的便是,则可用BFS来解决问题。
广度优先生成树:
因为广度遍历类似于树的层次遍历,所以在广度遍历中我们可以得到一课遍历树,称为广度优先生成树。
需要注意,邻接矩阵存储表示是唯一的,对应的广度优先生成树也是唯一的。而邻接表存储表示不唯一,所以其广度优先生成树也是不唯一的。
深度优先搜索
深度优先搜索类似于树的先序遍历。基本思想如下:
1)访问起始结点 v;
2)接着从 v 出发,访问 v 的第一个未访问过的邻接顶点
w
1
w_1
w1,并标记访问过得结点;
3)则将出队结点
w
1
w_1
w1视为起始结点v,回到第二步继续,直到不能继续访问;
4)若还有未访问结点,则选一个未访问节点来作为起始点,从第一步开始遍历,直到所有顶点都被访问为止
基于邻接矩阵的DFS代码实现如下:
bool visited[MAX_VERTEX_NUM];//访问标记数组
void DFS(MGraph G,int i){
cout<<G.vexs[i];
visited[i]=TRUE;
for (int j=0; j<G.vexnum; j++) {
if(G.edges[i][j]!=INFINITY&&!visited[j]&&i!=j){
DFS(G,j);
}
}
}
void DFSTracerse(MGraph G){
memset(visited, FALSE, sizeof(visited));//初始化数组
for (int i=0; i<G.vexnum; i++) {//从0号结点开始遍历
if (!visited[i]) {//该结点未被访问
DFS(G,i);//将该结点作为新一轮遍历的初始结点i进行遍历
}
}
cout<<endl;
}
基于邻接表的DFS代码如下:
bool visited[MAX_VERTEX_NUM];//访问标记数组
void DFS(ALGraph G,int i){
cout<<G.vertices[i].data;
visited[i]=TRUE;
for (ArcNode *p=G.vertices[i].first; p!=NULL;p=p->next) {
if(!visited[p->adjvex]){
DFS(G, p->adjvex);
}
}
}
void DFSTracerse(ALGraph G){
memset(visited, FALSE, sizeof(visited));//初始化数组
for (int i=0; i<G.vexnum; i++) {//从0号结点开始遍历
if (!visited[i]) {//该结点未被访问
DFS(G,i);//将该结点作为新一轮遍历的初始结点i进行遍历
}
}
cout<<endl;
}
图的邻接矩阵表示是唯一的,但对于邻接表来说,若边的输入次序不同,生成的邻接表也不同。
因此,对于同样一个图,基于邻接矩阵的遍历所得到的DFS序列和BFS序列都是唯一但,但基于邻接表的遍历得到DFS和BFS序列不唯一。
DFS算法性能分析:
1)空间复杂度:空间复杂度为 O ( ∣ V ∣ ) O(|V|) O(∣V∣),DFS算法是一个递归算法,因为需要借助一个递归工作栈。
2)时间复杂度:
1、以邻接矩阵表示,访问所有顶点所需时间为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),而访问任一顶点的邻接点所需时间为 O ( ∣ V ∣ ) O(|V|) O(∣V∣);
2、以邻接表表示,访问所有顶点所需时间为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣),而访问所有顶点的邻接点所需时间为 O ( ∣ E ∣ ) O(|E|) O(∣E∣)
Note:使用DFS算法递归的遍历一个无环有向图,并在退出递归时输出顶点,得到的序列是逆拓扑排序。
深度优先的生成树和生成森林
与广度优先搜索类似,深度优先搜索也会产生一棵深度优先树。不过,只有在连通图时调用DFS才能产生深度优先生成树,否则产生的会是深度优先生成森林
Notes:设无向图G=(V,E)的生成树为G’=(V’,E’),则有
1)G’为G的无环子图;
2)G’为G的极小连通子图且V’=V。
图的遍历与图的连通性
1、对无向图来说:若无向图是连通的,只需要调用一次DFS(G,i)或BFS(G,i)即可得到图中所有的顶点;而无向图是非连通的,则需要调用多次,且调用次数等于该图的连通分量数。
2、对有向图来说:它的连通子图分为强连通分量和非强连通分量,非强连通分量不能一次调用DFS(G,i)或BFS(G,i)即可得到该连通分量中所有的顶点。
6.4 图的应用
最小生成树
生成树:一个连通图的生成树包含图的所有顶点,并且只含尽可能少得边。对于生成树来说,若砍去它的一条边,则会使生成树变成非连通图;若给它增加一条边,则会形成图中的一条回路。
最小生成树:在一个带权连通无向图G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同,若T为G的所有生成树中权值最小的那棵生成树,称T为G的最小生成树。
最小生成树的性质如下:
1)最小生成树不是唯一的。
2)最小生成树的边权值之和总是唯一的,且是最小的。
3)最小生成树的边数为顶点数减一。
当带权连通图 G 中的任意一个环所包含的边权值互不相等时,G的最小生成树是唯一的;
若无向连通图 G 的边数比顶点数少1,即G本身是一棵树的时候,G的最小生成树就是它本身。
构成最小生成树算法主要有Prim算法和Kruskal算法(都是贪心算法)
Prim(普里姆)算法
Prim算法与Dijkstra算法类似:
假设 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中选择满足
{
(
u
,
v
)
∣
u
∈
U
,
v
∈
V
−
U
}
\{(u,v)|u\in U,v\in V-U\}
{(u,v)∣u∈U,v∈V−U}且权值最小的边
(
u
,
v
)
(u,v)
(u,v)加入树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)}
如下图所示:选
V
1
V_1
V1做为初始化
a、当前
U
=
{
V
1
}
U=\{V_1\}
U={V1},
E
T
=
∅
E_T=\varnothing
ET=∅
查找
V
1
V_1
V1的边,权值分别为6,1,5,其中1最小,选
(
V
1
,
V
3
)
(V_1,V_3)
(V1,V3)
b、当前
U
=
{
V
1
,
V
3
}
U=\{V_1,V_3\}
U={V1,V3},
E
T
=
{
(
V
1
,
V
3
)
}
E_T=\{(V_1,V_3)\}
ET={(V1,V3)}
查找
V
1
V_1
V1的边,权值分别为6,5,
查找
V
3
V_3
V3的边,权值分别为5,5,6,4,
其中4最小,选
(
V
3
,
V
6
)
(V_3,V_6)
(V3,V6)
c、当前
U
=
{
V
1
,
V
3
,
V
6
}
U=\{V_1,V_3,V_6\}
U={V1,V3,V6},
E
T
=
{
(
V
1
,
V
3
)
,
(
V
3
,
V
6
)
}
E_T=\{(V_1,V_3),(V_3,V_6)\}
ET={(V1,V3),(V3,V6)}
查找
V
1
V_1
V1的边,权值分别为6,5,
查找
V
3
V_3
V3的边,权值分别为5,5,6,
查找
V
6
V_6
V6的边,权值分别为2,6,
其中2最小,选
(
V
6
,
V
4
)
(V_6,V_4)
(V6,V4)
d、当前
U
=
{
V
1
,
V
3
,
V
6
,
V
4
}
U=\{V_1,V_3,V_6,V_4\}
U={V1,V3,V6,V4},
E
T
=
{
(
V
1
,
V
3
)
,
(
V
3
,
V
6
)
,
(
V
6
,
V
4
)
}
E_T=\{(V_1,V_3),(V_3,V_6),(V_6,V_4)\}
ET={(V1,V3),(V3,V6),(V6,V4)}
查找
V
1
V_1
V1的边,权值为6(原来的权值为5的边
(
V
1
,
V
4
)
(V_1,V_4)
(V1,V4)两个端点都被
U
U
U包括了所以舍去),
查找
V
3
V_3
V3的边,权值分别为5,6,
查找
V
6
V_6
V6的边,权值分别为6,
查找
V
4
V_4
V4的边,无(与该端点相关的边两个端点全都被
U
U
U包含了,所以全部被舍去),
其中5最小,选
(
V
3
,
V
2
)
(V_3,V_2)
(V3,V2)
e、当前
U
=
{
V
1
,
V
3
,
V
6
,
V
4
,
V
2
}
U=\{V_1,V_3,V_6,V_4,V_2\}
U={V1,V3,V6,V4,V2},
E
T
=
{
(
V
1
,
V
3
)
,
(
V
3
,
V
6
)
,
(
V
6
,
V
4
)
,
(
V
3
,
V
2
)
}
E_T=\{(V_1,V_3),(V_3,V_6),(V_6,V_4),(V_3,V_2)\}
ET={(V1,V3),(V3,V6),(V6,V4),(V3,V2)}
查找
V
1
V_1
V1的边,无
查找
V
3
V_3
V3的边,权值分别为6,
查找
V
6
V_6
V6的边,权值分别为6,
查找
V
4
V_4
V4的边,无,
查找
V
2
V_2
V2的边,权值分别为3,
其中3最小,选
(
V
2
,
V
5
)
(V_2,V_5)
(V2,V5)
f、获得最终最小生成树
U
=
{
V
1
,
V
3
,
V
6
,
V
4
,
V
2
,
V
5
}
U=\{V_1,V_3,V_6,V_4,V_2,V_5\}
U={V1,V3,V6,V4,V2,V5},
E
T
=
{
(
V
1
,
V
3
)
,
(
V
3
,
V
6
)
,
(
V
6
,
V
4
)
,
(
V
3
,
V
2
)
,
(
V
2
,
V
5
)
}
E_T=\{(V_1,V_3),(V_3,V_6),(V_6,V_4),(V_3,V_2),(V_2,V_5)\}
ET={(V1,V3),(V3,V6),(V6,V4),(V3,V2),(V2,V5)}
Prim算法时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),不依赖与 ∣ E ∣ |E| ∣E∣,所以更适用于求解边稠密的图的最小生成树(可用一些方法改进Prim算法时间复杂度,但实现会变得更加复杂)
Kruskal(克鲁斯卡尔)算法
与Prim算法从顶点开始扩展成最小生成树不同,Kruskal算法按权值的增第次序选择合适的边来构造最小生成树。
假设 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是最小生成树中边的集合。
初始化: U = V , E T = ∅ U=V,E_T=\varnothing U=V,ET=∅。即每个顶点都为一棵树,现在的 T 是一个含有 ∣ V ∣ |V| ∣V∣个顶点和 ∣ V ∣ |V| ∣V∣个树的森林
循环(重复下面步骤直到
E
T
E_T
ET 中含有
n
−
1
n-1
n−1条边):
按 G 的边的权值递增顺序从
E
−
E
T
E-E_T
E−ET中选择一条最小边,若这条加入 T 后不构成回路,则将其加入
E
T
E_T
ET,否则舍弃掉
如下图所示:
a、
U
=
{
V
1
,
V
2
,
V
3
,
V
4
,
V
5
,
V
6
}
,
E
T
=
∅
U=\{V_1,V_2,V_3,V_4,V_5,V_6\},E_T=\varnothing
U={V1,V2,V3,V4,V5,V6},ET=∅
找权值最小的边为1,
(
V
1
,
V
2
)
(V_1,V_2)
(V1,V2)
b、
E
T
=
{
(
V
1
,
V
2
)
}
E_T=\{(V_1,V_2)\}
ET={(V1,V2)}
找权值最小的边为2,
(
V
4
,
V
6
)
(V_4,V_6)
(V4,V6)
c、
E
T
=
{
(
V
1
,
V
2
)
,
(
V
4
,
V
6
)
}
E_T=\{(V_1,V_2),(V_4,V_6)\}
ET={(V1,V2),(V4,V6)}
找权值最小的边为3,
(
V
2
,
V
3
)
(V_2,V_3)
(V2,V3)
d、
E
T
=
{
(
V
1
,
V
2
)
,
(
V
4
,
V
6
)
,
(
V
2
,
V
3
)
}
E_T=\{(V_1,V_2),(V_4,V_6),(V_2,V_3)\}
ET={(V1,V2),(V4,V6),(V2,V3)}
找权值最小的边为4,
(
V
3
,
V
6
)
(V_3,V_6)
(V3,V6)
e、
E
T
=
{
(
V
1
,
V
2
)
,
(
V
4
,
V
6
)
,
(
V
2
,
V
3
)
,
(
V
3
,
V
6
)
}
E_T=\{(V_1,V_2),(V_4,V_6),(V_2,V_3),(V_3,V_6)\}
ET={(V1,V2),(V4,V6),(V2,V3),(V3,V6)}
找权值最小的边为5,且5的边有三条分别是
(
V
3
、
,
V
4
)
,
(
V
4
,
V
1
)
,
(
V
3
,
V
2
)
(V_3、,V_4),(V_4,V_1),(V_3,V_2)
(V3、,V4),(V4,V1),(V3,V2)
但代入
(
V
3
,
V
4
)
,
(
V
4
,
V
1
)
(V_3,V_4),(V_4,V_1)
(V3,V4),(V4,V1)会造成回路,所以舍去
选
(
V
3
,
V
2
)
(V_3,V_2)
(V3,V2)
f、
E
T
=
{
(
V
1
,
V
2
)
,
(
V
4
,
V
6
)
,
(
V
2
,
V
3
)
,
(
V
3
,
V
6
)
,
(
V
3
,
V
2
)
}
E_T=\{(V_1,V_2),(V_4,V_6),(V_2,V_3),(V_3,V_6),(V_3,V_2)\}
ET={(V1,V2),(V4,V6),(V2,V3),(V3,V6),(V3,V2)},共有6个顶点,5条边,已构成一棵树,得到最小生成树
Kruskal算法的时间复杂度为 O ( ∣ E ∣ l o g ∣ E ∣ ) , O(|E|log|E|), O(∣E∣log∣E∣),在Kruskal算法中,通常采用堆来存放边的集合,因此每次选择最小的权值的边只需 O ( l o g ∣ E ∣ ) O(log|E|) O(log∣E∣)的时间。Kruskal算法适合边稀疏而顶点较多的图。
最短路径
广度优先搜索查找最短路径只是对无权图而言的。当图是带权图时,把从一个顶点 v 0 v_0 v0 到图中其余任意一个顶点 v i v_i vi 的一条路径(可能不止一条)所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的那条路径称为最短路径。
带权有向图 G 的最短路径问题一般可分为两类:
一)单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可通过Dijkstra(迪杰斯特拉)算法求解。而Dijkstra算法也是贪心算法。
二)求每对顶点间的最短路径,可通过Floyd(弗洛伊德)算法求解
Dijkstra(迪杰斯特拉)算法
用Dijkstra算法求解单源最短路径问题,
假设从顶点0出发,步骤如下:
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,...,n−1
2)从顶点集合
V
−
S
V-S
V−S 中选出
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] ∣ vi∈V−S },
v
j
v_j
vj 就是当前求得的一条从
v
0
v_0
v0 出发的最短路径的终点,令
S
=
S
∪
{
j
}
S=S\cup\{j\}
S=S∪{j}
3)修改从
v
0
v_0
v0 出发到集合
V
−
S
V-S
V−S上任意顶点
v
k
v_k
vk可达的最短路径长度:若
d
i
s
t
[
j
]
+
a
r
c
s
[
j
]
[
k
]
<
d
i
s
t
[
k
]
dist[j]+arcs[j][k]<dist[k]
dist[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
n−1 次,直到所有顶点都包含在
S
S
S 中
如下图所示:
1)初始化:集合
S
S
S 初始为
{
v
1
}
\{v_1\}
{v1},
v
1
v_1
v1可达
v
2
v_2
v2和
v
5
v_5
v5,
v
1
v_1
v1不可达
v
3
v_3
v3和
v
4
v_4
v4,所以
d
i
s
t
[
]
dist[]
dist[]数组初值依次设置为
d
i
s
t
[
2
]
=
10
,
d
i
s
t
[
3
]
=
∞
,
d
i
s
t
[
4
]
=
∞
,
d
i
s
t
[
5
]
=
5
dist[2]=10,dist[3]=\infty,dist[4]=\infty,dist[5]=5
dist[2]=10,dist[3]=∞,dist[4]=∞,dist[5]=5,
选出最小值
d
i
s
t
[
5
]
dist[5]
dist[5],将
v
5
v_5
v5 并入集合
S
S
S ,即此时已找到
v
1
v_1
v1 和
v
5
v_5
v5 的最短路径。
2)集合
S
S
S 为
{
v
1
,
v
5
}
\{v_1,v_5\}
{v1,v5},更新
d
i
s
t
[
]
dist[]
dist[]数组。
v
5
v_5
v5 可达到
v
2
v_2
v2 且
v
1
v_1
v1->
v
5
v_5
v5->
v
2
v_2
v2 的长度为 8 比原来的
v
1
v_1
v1->
v
2
v_2
v2 ,
d
i
s
t
[
2
]
=
10
dist[2]=10
dist[2]=10小,所以更新
d
i
s
t
[
2
]
=
8
dist[2]=8
dist[2]=8;
v
5
v_5
v5 可达到
v
3
v_3
v3 且
v
1
v_1
v1->
v
5
v_5
v5->
v
3
v_3
v3 的长度为 14,更新
d
i
s
t
[
3
]
=
14
dist[3]=14
dist[3]=14;
v
5
v_5
v5 可达到
v
4
v_4
v4 且
v
1
v_1
v1->
v
5
v_5
v5->
v
4
v_4
v4 的长度为 7 ,更新
d
i
s
t
[
4
]
=
7
dist[4]=7
dist[4]=7;
选出最小值
d
i
s
t
[
4
]
dist[4]
dist[4],将
v
4
v_4
v4 并入集合
S
S
S ,即此时已找到
v
1
v_1
v1 和
v
4
v_4
v4 的最短路径。
3)集合
S
S
S 为
{
v
1
,
v
5
,
v
4
}
\{v_1,v_5,v_4\}
{v1,v5,v4},更新
d
i
s
t
[
]
dist[]
dist[]数组。
v
4
v_4
v4 不可达到
v
2
v_2
v2 ,不更新
d
i
s
t
[
2
]
=
8
dist[2]=8
dist[2]=8;
v
4
v_4
v4 可达到
v
3
v_3
v3 且
v
1
v_1
v1->
v
5
v_5
v5->
v
4
v_4
v4->
v
3
v_3
v3 的长度为 13 比原来的
v
1
v_1
v1->
v
5
v_5
v5->
v
3
v_3
v3 ,
d
i
s
t
[
3
]
=
14
dist[3]=14
dist[3]=14 小,更新
d
i
s
t
[
4
]
=
13
dist[4]=13
dist[4]=13 ;
选出最小值
d
i
s
t
[
2
]
dist[2]
dist[2],将
v
2
v_2
v2 并入集合
S
S
S ,即此时已找到
v
1
v_1
v1 和
v
2
v_2
v2 的最短路径。
4)集合
S
S
S 为
{
v
1
,
v
5
,
v
4
,
v
2
}
\{v_1,v_5,v_4,v_2\}
{v1,v5,v4,v2},更新
d
i
s
t
[
]
dist[]
dist[]数组。
v
2
v_2
v2 可达到
v
3
v_3
v3 且
v
1
v_1
v1->
v
5
v_5
v5->
v
2
v_2
v2->
v
3
v_3
v3 的长度为 9 比原来的
v
1
v_1
v1->
v
5
v_5
v5->
v
4
v_4
v4->
v
3
v_3
v3 ,
d
i
s
t
[
3
]
=
13
dist[3]=13
dist[3]=13小,更新
d
i
s
t
[
3
]
=
9
dist[3]=9
dist[3]=9;
选出唯一的最小值
d
i
s
t
[
3
]
dist[3]
dist[3],将
v
3
v_3
v3 并入集合
S
S
S ,即此时已找到
v
1
v_1
v1 和
v
3
v_3
v3 的最短路径。
此时全部顶点都包含在
S
S
S 中,算法结束。
用邻接矩阵表示的Dijkstra代码实现如下:
#include <iostream> //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;
#include <string.h>
#include <cstring>
#include <string>
typedef int Status;
#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
#define INFINITY 9999 //9999代表无穷,也就是两顶点不互通
#define MAX_VERTEX_NUM 20 //图最大顶点个数
typedef int EdgeType;//带权图中边上权值的数据类型
typedef string VertexType;
typedef struct {
VertexType vexs[MAX_VERTEX_NUM];//顶点向量
EdgeType edges[MAX_VERTEX_NUM][MAX_VERTEX_NUM];//图的邻接矩阵
int vexnum,arcnum;//图的当前顶点数和弧数
} MGraph;
struct dijtable{
string path;//走过的结点路径
int cost;//当前结点路径所花费的开销
bool elect;//当前方案是否已经成为最短路径最优解
};
int findvex(MGraph mg,string v){
//找到顶点v在mg中对应的位置,找不到返回-1
for (int i=0; i<mg.vexnum; i++) {
if(v==mg.vexs[i]) return i;
}
return -1;
}
void ShortestPath_DIJ(MGraph mg,string v){
dijtable dij[MAX_VERTEX_NUM];
//用迪杰斯特拉算法求有向网mg的顶点v到其余顶点的最短路径
int c=findvex(mg,v);
for (int i=0; i<mg.vexnum; i++) {
if(i!=c){
dij[i].cost=mg.edges[c][i];
dij[i].path=v+","+mg.vexs[i];
dij[i].elect=false;
}
else{
dij[i].elect=true;
dij[i].cost=0;
dij[i].path="";
}
}
for (int i=0; i<mg.vexnum; i++) {
int minc=INFINITY;
int flag=c;
string s;
for (int i=0; i<mg.vexnum; i++) {
if(dij[i].elect!=true){
if(minc>dij[i].cost){
minc=dij[i].cost;
flag=i;
}
}
}
if(minc!=INFINITY){
dij[flag].elect=true;
s=dij[flag].path;
}
//更新最短距离表
for (int i=0; i<mg.vexnum; i++) {
if(dij[i].elect==false){
if(minc+mg.edges[flag][i]<dij[i].cost){
dij[i].cost=minc+mg.edges[flag][i];
dij[i].path=s+","+mg.vexs[i];
}
}
}
}
cout<<"从"<<v<<"到各顶点的最短路径和所用的最少的开销:\n";
for (int i=0; i<mg.vexnum; i++) {
if(dij[i].cost!=INFINITY){
cout<<v<<"->"<<mg.vexs[i]<<":"<<dij[i].path<<" "<<dij[i].cost<<"\n";
}
else{
cout<<v<<"->"<<mg.vexs[i]<<":"<<"无法相通"<<"\n";
}
}
return;
}
int main() {
//将图录进去
MGraph mg;
string s[]={"v0","v1","v2","v3","v4","v5"};
mg.vexnum=6;
for (int i=0; i<mg.vexnum; i++) {
mg.vexs[i]=s[i];
}
EdgeType arcs[6][6]={
{INFINITY,INFINITY,10,INFINITY,30,100},
{INFINITY,INFINITY,5,INFINITY,INFINITY,INFINITY},
{INFINITY,INFINITY,INFINITY,50,INFINITY,INFINITY},
{INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,10},
{INFINITY,INFINITY,INFINITY,20,INFINITY,60},
{INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY}
};
mg.arcnum=8;
for (int i=0; i<mg.vexnum; i++) {
for (int j=0; j<mg.vexnum; j++) {
mg.edges[i][j]=arcs[i][j];
}
}
//找最短路径
ShortestPath_DIJ(mg,"v0");
return 0;
}
Dijkstra算法性能分析:
1)使用邻接矩阵表示时,时间复杂度为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)。
2)使用带权的邻接表表示时,时间复杂度仍为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2),不过修改
d
i
s
t
[
]
dist[]
dist[] 的时间会有所减少。
3)人们虽然有时候只是希望用算法来找到源点到某个特定顶点的最短路径,但这个问题和求解源点到其他所有顶点的最短路径是一样复杂的,时间复杂度也为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)
4)想利用Dijkstra算法解决每对顶点之间的最短路径问题。轮流将每个顶点作为源点,运行一次算法,所以时间复杂度为
O
(
∣
V
∣
3
)
O(|V|^3)
O(∣V∣3)
值得注意的是,边上如果带有负权值时,Dijkstra算法并不适用,但Dijkstra算法在带权无向图中适用,因为带权无向图可视为权值相同往返二重边的有向图
Floyd(弗洛伊德)算法
用Floyd算法求各顶点之间最短路径问题:
定义一个
n
n
n 阶方阵序列
A
−
1
,
A
0
,
A
1
,
.
.
.
A
n
−
1
A^{-1},A^0,A^1,...A^{n-1}
A−1,A0,A1,...An−1,其中
A
−
1
[
i
]
[
j
]
=
a
r
c
s
[
i
]
[
j
]
A^{-1}[i][j]=arcs[i][j]
A−1[i][j]=arcs[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
Ak[i][j]=Min{Ak−1[i][j],Ak−1[i][k]+Ak−1[k][j]},k=0,1,...,n−1
式中,
A
0
[
i
]
[
j
]
A^{0}[i][j]
A0[i][j]是从顶点
v
i
v_i
vi到
v
j
v_j
vj、中间顶点是
v
0
v_0
v0的最短路径长度。
Floyd算法是迭代的过程,每迭代一次,在从顶点
v
i
v_i
vi到
v
j
v_j
vj的最短路径上又多考虑了一个顶点;
经过n次迭代后,所得到的
A
n
−
1
[
i
]
[
j
]
A^{n-1}[i][j]
An−1[i][j]就是从顶点
v
i
v_i
vi到
v
j
v_j
vj的最短路径长度
即方阵
A
n
−
1
A^{n-1}
An−1保存了任意一对顶点之间的最短路径长度。
如下图所示:
初始化:方阵
A
−
1
[
i
]
[
j
]
=
a
r
c
s
[
i
]
[
j
]
A^{-1}[i][j]=arcs[i][j]
A−1[i][j]=arcs[i][j]
1)将
v
0
v_0
v0作为中间顶点,对于所有顶点对
{
i
,
j
}
\{i,j\}
{i,j},如果有
A
−
1
[
i
]
[
j
]
>
A
−
1
[
i
]
[
0
]
+
A
−
1
[
0
]
[
j
]
A^{-1}[i][j]>A^{-1}[i][0]+A^{-1}[0][j]
A−1[i][j]>A−1[i][0]+A−1[0][j],则将
A
−
1
[
i
]
[
j
]
=
A
−
1
[
i
]
[
0
]
+
A
−
1
[
0
]
[
j
]
A^{-1}[i][j]=A^{-1}[i][0]+A^{-1}[0][j]
A−1[i][j]=A−1[i][0]+A−1[0][j]。
也就是以
v
0
v_0
v0作为中转点,有没有一个路径比原来两点直达更优的。
在下图中,有
A
−
1
[
2
]
[
1
]
>
A
−
1
[
2
]
[
0
]
+
A
−
1
[
0
]
[
2
]
=
11
A^{-1}[2][1]>A^{-1}[2][0]+A^{-1}[0][2]=11
A−1[2][1]>A−1[2][0]+A−1[0][2]=11,更新
A
−
1
[
2
]
[
1
]
=
11
A^{-1}[2][1]=11
A−1[2][1]=11,更新后的方阵标记为
A
0
A^0
A0
2)将
v
1
v_1
v1作为中间顶点,对于所有顶点对
{
i
,
j
}
\{i,j\}
{i,j},检测
A
0
[
i
]
[
j
]
>
A
0
[
i
]
[
1
]
+
A
0
[
1
]
[
j
]
A^{0}[i][j]>A^{0}[i][1]+A^{0}[1][j]
A0[i][j]>A0[i][1]+A0[1][j]。
有
A
0
[
0
]
[
2
]
>
A
0
[
0
]
[
1
]
+
A
0
[
1
]
[
2
]
=
10
A^{0}[0][2]>A^{0}[0][1]+A^{0}[1][2]=10
A0[0][2]>A0[0][1]+A0[1][2]=10,更新
A
0
[
0
]
[
2
]
=
10
A^{0}[0][2]=10
A0[0][2]=10,更新后的方阵标记为
A
1
A^1
A1
3)将
v
2
v_2
v2作为中间顶点,对于所有顶点对
{
i
,
j
}
\{i,j\}
{i,j},检测
A
1
[
i
]
[
j
]
>
A
1
[
i
]
[
2
]
+
A
1
[
2
]
[
j
]
A^{1}[i][j]>A^{1}[i][2]+A^{1}[2][j]
A1[i][j]>A1[i][2]+A1[2][j]。
有
A
1
[
1
]
[
0
]
>
A
1
[
1
]
[
2
]
+
A
1
[
2
]
[
0
]
=
9
A^{1}[1][0]>A^{1}[1][2]+A^{1}[2][0]=9
A1[1][0]>A1[1][2]+A1[2][0]=9,更新
A
1
[
1
]
[
2
]
=
9
A^{1}[1][2]=9
A1[1][2]=9,更新后的方阵标记为
A
2
A^2
A2
此时
A
2
A^2
A2中保存的就是任意顶点对的最短路径长度
Floyd算法性能分析:
Floyd算法的时间复杂度为
O
(
∣
V
∣
3
)
O(|V|^3)
O(∣V∣3),和利用Dijkstra算法解决每对顶点之间的最短路径的时间复杂度一样,但Floyd不包含其他复杂的数据结构,所以隐含的常数系数很小,即是对于中等规模的输入,它仍然相当有效。
Floyd算法允许图中带有负权值的边,但不能有包含带负权值的边组成的回路。而且Floyd算法在带权无向图中同样适用,因为带权无向图可视为权值相同往返二重边的有向图
有向无环图描述表达式
有向无环图:若一个有向图中不存在环,则称为有向无环图,简称是DAG图。
有向无环图是描述含有公共子式的表达式的有效工具。例如表达式:
(
(
a
+
b
)
∗
(
b
∗
(
c
+
d
)
)
+
(
c
+
d
)
∗
e
)
∗
(
(
c
+
d
)
∗
e
)
((a+b)*(b*(c+d))+(c+d)*e)*((c+d)*e)
((a+b)∗(b∗(c+d))+(c+d)∗e)∗((c+d)∗e)
该表达式可以用上一章的二叉树来表示,如下图6.20所示。
仔细观察该表达式,可发现有一些相同的子表达式
(
c
+
d
)
(c+d)
(c+d)和
(
c
+
d
)
∗
e
(c+d)*e
(c+d)∗e,而在二叉树中,它们也重复出现。若利用有向无环图,则可实现对相同子式的共享,从而节省空间,如下图6.21所示。
拓扑排序
AOV网:若用DAG图表示一个工程,其顶点表示活动,用有向边
<
V
i
,
V
j
>
<V_i,V_j>
<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 不能以它自己作为自己的前驱或后驱。
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
1)每个顶点出现且只出现一次。
2)若顶点 A 在序列中排在顶点 B 的前面,则在图中不存在从顶点 B 到顶点 A 的路径
或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点 A 到顶点 B 的路径,则在排序中顶点 B 出现在顶点 A 的后面。
每个AOV网都有一个或多个拓扑排序,且有向无环图的拓扑排序唯一并不能确定该图
下面介绍一个比较常用的拓扑排序的算法:
1)从AOV网中选择一个没有前驱的顶点并输出。
2)从网中删除该顶点和所有以它为起点的有向边。
3)重复1和2步直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环,换句话说,该有向图含有顶点数目大于1的强连通分量
若一个有向图的邻接矩阵为三角矩阵(对角线上元素为0),则图中并不存在环,因此其拓扑序列必然存在。
相对的,只要有向图可以进行有序的拓扑排序,我们就可以对有向图的顶点进行适当的编号,使其邻接矩阵为三角矩阵且主对角元全为0
算法具体代码如下:
#include <iostream> //C++头文件格式,如果需要可替换成C语言的头文件
using namespace std;
#include <string.h>
#include <cstring>
#include <string>
#include <stack>
#include <queue>
typedef int Status;
#define error -1
//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
#define INFINITY 9999 //9999代表无穷,也就是两顶点不互通
#define MAX_VERTEX_NUM 20 //图最大顶点个数
typedef string VertexType;//图的结点数据类型
typedef int EdgeType;
typedef struct {
VertexType vexs[MAX_VERTEX_NUM];//顶点向量
EdgeType edges[MAX_VERTEX_NUM][MAX_VERTEX_NUM];//图的邻接矩阵
int vexnum,arcnum;//图的当前顶点数和弧数
} MGraph;
int indegree[MAX_VERTEX_NUM];
void FindIndegree(MGraph G){
memset(indegree, 0, sizeof(indegree));
for (int i=0; i<G.vexnum; i++) {
for (int j=0; j<G.vexnum; j++) {
if(G.edges[i][j]!=INFINITY){
indegree[j]++;
}
}
}
}
bool TopologicalSort(MGraph G){
stack<int> s;
int i,j;
FindIndegree(G);//构造结点入度表
for (i=0; i<G.vexnum; i++) {
//查找入度为0的结点进栈中
if(indegree[i]==0){
s.push(i);
}
}
int count=0;//记录当前已经输出的顶点数
while(!s.empty()){
i=s.top();
s.pop();
cout<<G.vexs[i];
count++;
//将所有i所指向的顶点入度减一,并将入度减为0的顶点压入栈中
for(j=0;j<G.vexnum;j++){
if(G.edges[i][j]!=INFINITY){
if(!(--indegree[j])){
s.push(j);
}
}
}
}
cout<<endl;
if(count<G.vexnum){
return false;//排序失败,有向图有回路
}
return true;//拓扑排序成功
}
int main(){
MGraph mg;
string s[]={"v1","v2","v3","v4","v5"};
mg.vexnum=5;
for (int i=0; i<mg.vexnum; i++) {
mg.vexs[i]=s[i];
}
EdgeType arcs[5][5]={
{INFINITY,1,INFINITY,1,INFINITY},
{INFINITY,INFINITY,1,1,INFINITY},
{INFINITY,INFINITY,INFINITY,INFINITY,1},
{INFINITY,INFINITY,1,INFINITY,1},
{INFINITY,INFINITY,INFINITY,INFINITY,INFINITY}
};
mg.arcnum=7;
for (int i=0; i<mg.vexnum; i++) {
for (int j=0; j<mg.vexnum; j++) {
mg.edges[i][j]=arcs[i][j];
}
}
TopologicalSort(mg);
return 0;
}
在上面的拓扑排序算法中,为暂存入度为0的顶点,既可以如上面一样栈来存,也可以用队列来存。
由于输出每个顶点的同时,还要删除以它为起点的边,所以有:
由邻接表表示的图,拓扑排序的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
由邻接矩阵表示的图,拓扑排序的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
对一个AOV网,如果采用下列步骤进行排序,则称为逆拓扑排序:
1)从AOV网中,选择一个没有后继(出度为0)的顶点输出。
2)从网中删除该结点以及所以以它为终点的有向边。
3)重复1和2直到当前AOV网为空。
当有向图中无环时,我们也可以利用深度优先遍历进行拓扑排序,因为无环,则由图中某点出发进行深度遍历搜索时,最先退出DFS函数的顶点一定为出度为0的顶点,就也是拓扑排序最后一个顶点。由此在DFS中退出递归栈之前输出,会得到一个逆向的拓扑有序序列,也就是逆拓扑排序。
利用拓扑排序算法处理AOV网,应该注意一下问题:
1)入度为0的顶点,即没有前驱活动的或者前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续。
2)若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一。若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。
3)由于AOV网中各顶点地位平等,每个顶点编号是人为的,由此可以按拓扑排序的结果重新编号,生成AOV网的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于一般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑排序,反之则不一定成立。
关键路径
AOE网:在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网。
AOE网和AOV网都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,在AOV网中顶点代表活动,AOE网中边代表活动;
AOE网的边有权值,而AOV网中的边无权值,仅表示顶点之间的前后关系。
AOE网具有以下性质:
1)只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。
2)只有在进入某顶点的各有向边所代表的的活动都已经结束的时候,该顶点所代表的事件才能发生
源点(开始顶点):在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它代表整个工程的开始。
汇点(结束顶点):网中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它代表整个工程的结束
关键路径、关键活动:从源点到汇点的有向路径中可能有多条,并且这些路径长度可能不同,但是只有所有路径上的活动都已完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
寻找关键路径的方法:
1.参数概念:
(1)事件
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
,
v
e
(
k
)
=
M
a
x
{
v
e
(
j
)
+
W
e
i
g
h
t
(
v
j
,
v
k
)
}
ve(源点)=0\ \ ,\ \ ve(k)=Max\{ve(j)+Weight(v_j,v_k)\}
ve(源点)=0 , 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
>
<v_j,v_k>
<vj,vk>上的权值。
v
e
(
k
)
ve(k)
ve(k)的计算方法:
按从前往后的顺序进行,在拓扑排序的方法基础上进行计算:
① 初始时,令
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) 。以此类推,直到输出全部顶点。
(2)事件
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
(
汇
点
)
,
v
l
(
k
)
=
M
i
n
{
v
l
(
j
)
−
W
e
i
g
h
t
(
v
k
,
v
j
)
}
vl(汇点)=ve(汇点)\ \ ,\ \ vl(k)=Min\{vl(j)-Weight(v_k,v_j)\}
vl(汇点)=ve(汇点) , vl(k)=Min{vl(j)−Weight(vk,vj)}其中
v
k
v_k
vk 为
v
j
v_j
vj 的任意前驱。
v
l
(
k
)
vl(k)
vl(k)的计算方法:
按从前往后的顺序进行,在逆拓扑排序的方法基础上进行计算(在上述的拓扑排序中,增加一个栈以记录拓扑排序结束后从栈顶到栈底便为逆拓扑有序序列):
① 初始时,令
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) 。以此类推,直到输出栈中的全部顶点。
(3)活动
a
i
a_i
ai 的最早开始时间
e
(
i
)
e(i)
e(i) :
它是指该活动弧的起点所表示的事件的最早发生时间。若边
<
v
k
,
v
j
>
<v_k,v_j>
<vk,vj> 表示活动
a
i
a_i
ai 则有
e
(
i
)
=
v
e
(
k
)
e(i)=ve(k)
e(i)=ve(k)。
(4)活动
a
i
a_i
ai 的最迟开始时间
l
(
i
)
l(i)
l(i) :
它是指该活动弧的终点所表示的事件的最迟发生时间与该活动所需时间之差。若边
<
v
k
,
v
j
>
<v_k,v_j>
<vk,vj> 表示活动
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)。
(5)一个活动
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 可以拖延的时间。若一个活动的时间余量为0,则说明该活动必须要如期完成,否则会拖延整个工程进度,所以称
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 是关键活动。
2.求关键路径算法步骤:
求关键路径的算法步骤如下:
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)求 v e ( ) ve() ve() :初始 v e ( 1...6 ) = 0 ve(1...6)=0 ve(1...6)=0 在拓扑排序输出顶点:
1.1 查找入度为0的顶点
v
1
v_1
v1,且它的直接后继顶点
v
e
(
2
)
=
M
a
x
{
v
e
(
2
)
,
v
e
(
1
)
+
w
i
g
h
t
(
v
1
,
v
2
)
}
ve(2)=Max\{ve(2),ve(1)+wight(v_1,v_2)\}
ve(2)=Max{ve(2),ve(1)+wight(v1,v2)}=3
v
e
(
3
)
=
M
a
x
{
v
e
(
3
)
,
v
e
(
1
)
+
w
i
g
h
t
(
v
1
,
v
3
)
}
ve(3)=Max\{ve(3),ve(1)+wight(v_1,v_3)\}
ve(3)=Max{ve(3),ve(1)+wight(v1,v3)}=2
已经没有直接后继顶点了,删除
v
1
v_1
v1及以其为起点的边
1.2 查找入度为0的顶点
v
2
v_2
v2 和
v
3
v_3
v3,所以
v
e
(
2
)
=
3
,
v
e
(
3
)
=
2
ve(2)=3,ve(3)=2
ve(2)=3,ve(3)=2固定下来
先看
v
2
v_2
v2的直接后继顶点
v
e
(
4
)
=
M
a
x
{
v
e
(
4
)
,
v
e
(
2
)
+
w
i
g
h
t
(
v
2
,
v
4
)
}
ve(4)=Max\{ve(4),ve(2)+wight(v_2,v_4)\}
ve(4)=Max{ve(4),ve(2)+wight(v2,v4)}=5
v
e
(
5
)
=
M
a
x
{
v
e
(
5
)
,
v
e
(
2
)
+
w
i
g
h
t
(
v
2
,
v
5
)
}
ve(5)=Max\{ve(5),ve(2)+wight(v_2,v_5)\}
ve(5)=Max{ve(5),ve(2)+wight(v2,v5)}=6
再看
v
3
v_3
v3的直接后继顶点
v
e
(
4
)
=
M
a
x
{
v
e
(
4
)
,
v
e
(
3
)
+
w
i
g
h
t
(
v
3
,
v
4
)
}
=
M
a
x
{
5
,
6
}
=
6
ve(4)=Max\{ve(4),ve(3)+wight(v_3,v_4)\}=Max\{5,6\}=6
ve(4)=Max{ve(4),ve(3)+wight(v3,v4)}=Max{5,6}=6
v
e
(
6
)
=
M
a
x
{
v
e
(
6
)
,
v
e
(
3
)
+
w
i
g
h
t
(
v
3
,
v
6
)
}
=
5
ve(6)=Max\{ve(6),ve(3)+wight(v_3,v_6)\}=5
ve(6)=Max{ve(6),ve(3)+wight(v3,v6)}=5
已经没有直接后继顶点了,删除
v
2
,
v
3
v_2,v_3
v2,v3 及以其为起点的边
1.3 查找入度为0的顶点
v
4
v_4
v4 和
v
5
v_5
v5,所以
v
e
(
4
)
=
6
,
v
e
(
5
)
=
6
ve(4)=6,ve(5)=6
ve(4)=6,ve(5)=6固定下来
先看
v
4
v_4
v4的直接后继顶点
v
e
(
6
)
=
M
a
x
{
v
e
(
6
)
,
v
e
(
4
)
+
w
i
g
h
t
(
v
4
,
v
6
)
}
=
M
a
x
{
5
,
8
}
=
8
ve(6)=Max\{ve(6),ve(4)+wight(v_4,v_6)\}=Max\{5,8\}=8
ve(6)=Max{ve(6),ve(4)+wight(v4,v6)}=Max{5,8}=8
再看
v
5
v_5
v5的直接后继顶点
v
e
(
6
)
=
M
a
x
{
v
e
(
6
)
,
v
e
(
5
)
+
w
i
g
h
t
(
v
5
,
v
6
)
}
=
M
a
x
{
8
,
7
}
=
8
ve(6)=Max\{ve(6),ve(5)+wight(v_5,v_6)\}=Max\{8,7\}=8
ve(6)=Max{ve(6),ve(5)+wight(v5,v6)}=Max{8,7}=8
已经没有直接后继顶点了,删除
v
4
,
v
5
v_4,v_5
v4,v5 及以其为起点的边
1.4 查找入度为0的顶点 v 6 v_6 v6,所以 v e ( 6 ) = 8 ve(6)=8 ve(6)=8固定下来,得到全部顶点的ve()表,且可以得到拓扑排序序列为 { v 1 , v 2 , v 3 , v 4 , v 5 , v 6 } \{v_1,v_2,v_3,v_4,v_5,v_6\} {v1,v2,v3,v4,v5,v6}依次放入栈中
如果这是选择题,根据上述求解过程就已经知道关键路径了
2)求 v l ( ) vl() vl() :初始 v l ( 1...6 ) = 8 vl(1...6)=8 vl(1...6)=8 ,并且出栈输出顶点:
2.1 出栈顶点
v
6
v_6
v6,且它的直接前继顶点
v
l
(
5
)
=
M
i
n
{
v
l
(
5
)
,
v
l
(
6
)
−
w
i
g
h
t
(
v
5
,
v
6
)
}
=
M
i
n
{
8
,
7
}
=
7
vl(5)=Min\{vl(5),vl(6)-wight(v_5,v_6)\}=Min\{8,7\}=7
vl(5)=Min{vl(5),vl(6)−wight(v5,v6)}=Min{8,7}=7
v
l
(
4
)
=
M
i
n
{
v
l
(
4
)
,
v
l
(
6
)
−
w
i
g
h
t
(
v
4
,
v
6
)
}
=
6
vl(4)=Min\{vl(4),vl(6)-wight(v_4,v_6)\}=6
vl(4)=Min{vl(4),vl(6)−wight(v4,v6)}=6
v
l
(
3
)
=
M
i
n
{
v
l
(
3
)
,
v
l
(
6
)
−
w
i
g
h
t
(
v
3
,
v
6
)
}
=
5
vl(3)=Min\{vl(3),vl(6)-wight(v_3,v_6)\}=5
vl(3)=Min{vl(3),vl(6)−wight(v3,v6)}=5
已经没有直接前继顶点了,删除
v
6
v_6
v6及以其为终点的边
2.2 出栈顶点
v
5
v_5
v5,则
v
l
(
5
)
vl(5)
vl(5) 确定下来,且它的直接前继顶点
v
l
(
2
)
=
M
i
n
{
v
l
(
2
)
,
v
l
(
5
)
−
w
i
g
h
t
(
v
2
,
v
5
)
}
=
M
i
n
{
8
,
4
}
=
4
vl(2)=Min\{vl(2),vl(5)-wight(v_2,v_5)\}=Min\{8,4\}=4
vl(2)=Min{vl(2),vl(5)−wight(v2,v5)}=Min{8,4}=4
已经没有直接前继顶点了,删除
v
5
v_5
v5及以其为终点的边
2.3 出栈顶点
v
4
v_4
v4,则
v
l
(
5
)
vl(5)
vl(5) 确定下来,且它的直接前继顶点
v
l
(
3
)
=
M
i
n
{
v
l
(
3
)
,
v
l
(
4
)
−
w
i
g
h
t
(
v
3
,
v
4
)
}
=
M
i
n
{
5
,
2
}
=
2
vl(3)=Min\{vl(3),vl(4)-wight(v_3,v_4)\}=Min\{5,2\}=2
vl(3)=Min{vl(3),vl(4)−wight(v3,v4)}=Min{5,2}=2
已经没有直接前继顶点了,删除
v
4
v_4
v4及以其为终点的边
2.4 出栈顶点
v
3
v_3
v3,则
v
l
(
4
)
vl(4)
vl(4) 确定下来,且它的直接前继顶点
v
l
(
1
)
=
M
i
n
{
v
l
(
1
)
,
v
l
(
3
)
−
w
i
g
h
t
(
v
1
,
v
3
)
}
=
0
vl(1)=Min\{vl(1),vl(3)-wight(v_1,v_3)\}=0
vl(1)=Min{vl(1),vl(3)−wight(v1,v3)}=0
已经没有直接前继顶点了,删除
v
3
v_3
v3及以其为终点的边
2.5 出栈顶点
v
2
v_2
v2,则
v
l
(
3
)
vl(3)
vl(3) 确定下来,且它的直接前继顶点
v
l
(
1
)
=
M
i
n
{
v
l
(
1
)
,
v
l
(
2
)
−
w
i
g
h
t
(
v
1
,
v
2
)
}
=
0
vl(1)=Min\{vl(1),vl(2)-wight(v_1,v_2)\}=0
vl(1)=Min{vl(1),vl(2)−wight(v1,v2)}=0
已经没有直接前继顶点了,删除
v
2
v_2
v2及以其为终点的边
2.6 出栈顶点 v 1 v_1 v1,则 v l ( 2 ) vl(2) vl(2) 确定下来,且已经栈空,则 v l ( 1 ) vl(1) vl(1) 也随之确定,得到v
3)弧的最早开始时间
e
(
i
)
e(i)
e(i) 等于该弧的起点的顶点的
v
e
(
)
ve()
ve() 。
4)弧的最迟开始时间
l
(
i
)
l(i)
l(i) 等于该弧的终点的顶点的
v
l
(
)
vl()
vl() 减去该弧持续的时间。
5)根据
l
(
i
)
=
e
(
i
)
l(i)=e(i)
l(i)=e(i)的关键活动,得到关键路径为
(
v
1
,
v
3
,
v
4
,
v
6
)
(v_1,v_3,v_4,v_6)
(v1,v3,v4,v6)
1)关键路径上的所有活动都是关键活动,它是绝对整个工程的关键因数,因此可以通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定程度,该关键活动就可能变成非关键活动。
2)网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。