ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

数据结构(C语言版)严蔚敏 吴伟民 编著 第7章 图

2021-02-24 17:02:44  阅读:175  来源: 互联网

标签:结点 有向图 vi 路径 C语言 严蔚敏 邻接 顶点 数据结构


数据结构(C语言版)严蔚敏 吴伟民 编著 第7章 图

前言

在图形结构中,结点之间的关系可以是任意的,图中任意两个元素之间都可能相关。由此,图的应用极为广泛,已渗入到诸如语言学、逻辑学、物理、化学、电讯工程、计算机科学以及数学的其他分支中。

7.1 图的定义和术语

在图中的数据元素通常称为顶点,V是顶点的有穷非空集合,VR是两个顶点之间的关系的集合。若<v,w>∈VR,则<v,w>表示从v到w的一条弧,且称v为弧尾或初始点,称w为弧头或终端点,此时的图称为有向图。若<v,w>∈VR必有<w,v>∈VR,即VR是对称的,则以无序对(v,w)代替这两个有序对,表示v到w之间的一条边,此时的图称为无向图。
我们用n表示图中顶点数目,用e表示边或弧的数目。在下面的讨论中,我们不考虑顶点到其自身的弧或边,即若<vi,vj>∈VR,则vi≠vj,那么对于无向图,e的取值范围是0到n(n+1)/2,有n(n+1)/2条边的无向图称为完全图。对于有向图,e的取值范围是0到n(n+1)。具有n(n+1)条弧的有向图称为有向完全图。有很少条边或弧(如e<nlogn)的图称为稀疏图,反之称为稠密图。
有时图的边或弧具有与它相关的数,这种与图的边或弧相关的数叫做权。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网。
对于两个图G=(V,{E})和G’=(V’,{E’}),如果V’⊆V且E’⊆E,则称G’为G的子图。
对于无向图G=(V,{E}),如果边(v,v’)∈E,则称顶点v和v’互为邻接点,即v和v’相邻接。边(v,v’)依附于顶点v和v’,或者说(v,v’)和顶点v和v’相关联。顶点v的度是和v相关联的边的数目,记为TD(V)。对于有向图G=(V,{A}),如果弧<v,v’>∈A,则称顶点v邻接v’,顶点v’邻接自顶点v。弧<v,v’>和顶点v,v’相关联。以顶点v为头的弧的数目称为v的入度,记为ID(V),以v为尾的弧的数目称为v的出度,记为OD(v),顶点v的度为TD(v)=ID(v)+OD(v)。一般地,如果顶点vi的度记为TD(vi),那么一个有n个顶点,e条边或弧的图,满足如下关系
e = ½ ∑ i = 1 n T D ( v i ) \sum_{i=1}^{n} TD(v_i) ∑i=1n​TD(vi​)
无向图G=(V,{E})中从顶点v到顶点v’的路径是一个顶点序列(v=vi,0,v=vi,1,…vi,m=v’),其中(vi,j-1,vi,j)∈E,1≤j≤m。如果G是有向图,则路径也是有向的,顶点序列应满足<vi,j-1,vi,j>∈E,1≤j≤m。路径的长度是路径上的边或弧的数目。第一个顶点和最后一个顶点相同的路径称为回路或环。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
在无向图中,如果从顶点v到顶点v’有路径,则称v和v’是连通的。如果对于图中任意两个顶点vi、vj∈V,vi和vj都是连通的,则称G是连通图。所谓连通分量指的是无向图中的极大连通子图。
在有向图中,如果对于每一对vi,vj∈V,vi≠vj,从vi到vj都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。一棵有n个顶点的生成树有且仅有n-1条边。如果一个图有n个顶点和小于n-1条边,则是非连通图。如果它多于n-1条边,则一定有环。但是有n-1条边的图不一定是生成树。
如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树,一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。
在前述图的基本操作的定义中,关于顶点的位置和邻接点的位置都只是一个相对的概念。因为,从图的逻辑结构的定义来看,图中的顶点之间不存在全序的关系(即无法将图中顶点排列成一个线性序列),任何一个顶点都可被看成是第一个顶点;另一方面,任一顶点的邻接点之间也不存在次序关系。但为了操作方便,我们需要将图中顶点按任意的顺序排列起来。这个排序和关系VR无关。由此,所谓顶点在图中的位置指的是该顶点在这个人为的任意排列中的位置或序号。同理,可对某个顶点的所有邻接点进行排队,在这个排队中自然形成了第一个或第k个邻接点。若某个顶点的邻接点的个数大于k,则称k+1个邻接点为第k个邻接点的下一个邻接点,而最后一个邻接点的下一个邻接点为空。

7.2 图的存储结构

图的结构复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序映像的存储结构,但可以借助数组的数据类型表示元素之间的关系。另一方面,用多重链表表示图是自然而然的事,它是一种最简单的链式映像结构,即以一个由一个数据域和多个指针域组成的结点表示其中一个结点,其中数据域存储该顶点的信息,指针域存储指向其邻接点的指针。但是由于图中各个结点的度数各不相同,最大度数和最小度数可能相差很多,因此若按度数最大的顶点设计结点结构,则会浪费很多存储单元,反之若按每个顶点自己的度数设计不同的结点结构,又会给操作带来不便。因此,和树类似,在实际应用中不宜采用这种结构,而应根据具体的图和需要进行的操作,设计恰当的结点结构和表结构。常见的有邻接表、邻接多重表和十字链表。

7.2.1 数组表示法

用两个数组分别存储数据元素(顶点)的信息和数据元素之间的关系(边或弧)的信息,其形式描述如下:

// 图的数组(邻接矩阵)存储表示
#define INFINITY        INT_MAX  // 最大值∞
#define MAX_VERTEX_NUM  20       // 最大顶点个数
typedef enum{DG,DN,UDG,UDN} GraphKind; // {有向图,有向网,无向图,无向网}
typedef struct ArcCell{
	VRType    adj;      // VRType是顶点关系类型。对无权图用1或0表示相邻否,对带权图,则为权值类型
	InfoType  *info;    // 该弧相关信息的指针
}ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef struct{
	VertexType vex[MAX_VERTEX_NUM];   // 顶点向量
	AdjMatrix  arcs;                  // 邻接矩阵
	int   vexnum, arcnum;             // 图的当前顶点数和弧数
	GraphKind   kind;                 // 图的种类标志
}MGraph;

以二维数组表示有n个顶点的图时,需存放n个顶点信息和n2个弧信息的存储量。若考虑无向图的邻接矩阵的对称性,则可采用压缩存储的方式只存入矩阵的下三角(或上三角)元素。
借助于邻接矩阵容易判定任意两个顶点之间是否有边(或弧)相连,并容易求得各个顶点的度。对于无向图,顶点vi的度是邻接矩阵中第i行或第i列的元素之和;对于有向图,第i行的元素之和为顶点vi的出度OD(vi),第j列的元素之和为顶点vj的入度ID(vj)。
网的邻接矩阵可定义为:
A[i][j] = wi,j ,若<vi,vj>或(vi,vj)∈VR
∞, 其他
下述算法是在邻接矩阵存储结构MGraph上对图的构造操作的实现框架,它根据图G的种类调用具体构造算法。如果G是无向网,则调用之后的算法,构造一个具有n个顶点和e条边的无向网的时间复杂度是O(n2+e×n),其中对邻接矩阵G.arcs的初始化耗费了O(n2)的时间。

Status CreateGraph(MGraph &G){
	// 采用数组(邻接矩阵)表示法,构造图G
	scanf(&G.kind);
	switch(G.kind){
		case  DG : return CreateDG(G);   // 构造有向图G
		case  DN : return CreateDN(G);   // 构造有向网G
		case UDG : return CreateUDG(G);  // 构造无向图G
		case UDN : return CreateUDN(G);  // 构造无向网G
		default: return ERROR;
	}
}// CreateGraph;

Status CreateUDN(MGraph &G){
	// 采用数组(邻接矩阵)表示法,构造无向网G
	scanf(&G.vexnum, &G.arcnum, &IncInfo); // IncInfo为0则各弧不含其它信息
	for(i = 0; i < G.vexnum; ++i) scanf(&G.vexs[i]); // 构造顶点向量
	for(i = 0; i < G.vexnum; ++i){           // 初始化邻接矩阵
		for(j = 0; j <G.vexnum; ++j) G.arcsur[i][j] = {INFINITY, NULL};  // {adj,info}
	for(k=0; k<G.arcnum; ++k){                   // 构造邻接矩阵
		scanf(&v1, &v2, &w);                     // 输入一条边依附的顶点及权值
		i= LocateVex(G,v1); j = LocateVex(G,v2); // 确定v1和v2在G中的位置
		G.arc[i][j].adj = w;                     // 弧<v1,v2>的权值
		if(IncInfo) Input(*G.arcs[i][j].info);   // 若弧含有相关信息,则输入
		G.arc[j][i] = G.arc[i][j];               // 置<v1,v2>的对称弧<v2,v1>
	}
	return OK;
}// CreateUDN

7.2.2 邻接表

在邻接表中,对图中每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边,对有向图是以顶点vi为尾的弧。每个结点由3个域组成,其中邻接点域(adjvex)指示与顶点vi邻接的点在图中的位置,链域(nextarc)指示下一个边或弧的的结点,数据域存储和边或弧相关的信息,如权值等。每个链表上附设一个表头结点,在表头结点中,除了设有链域(firstarc)指向链表中第一个结点以外,还设有存储顶点vi的名或其他有关信息的数据域(data)。
表结点:
adjvex nextarc info
头结点:
data firstarc
这些表头结点(可以链相接)通常以顺序结构的形式存储,以便随机访问任一结点的链表。一个图的邻接表存储结构可形式地说明如下:

// 图的邻接表存储表示
#define MAX_VERTEX_NUM 20   
typedef struct ArcNode{
	int             adjvex;    // 该弧所指向的顶点的位置
	struct AcrNode *nextarc;   // 指向下一条弧的指针
	InfoType       *info;      // 该弧相关信息的指针     
}ArcNode;

typedef struct VNode{
	VertexType  data;          // 顶点信息
	ArcNode    *firstar;       // 指向第一条依附该顶点的弧的指针
}VNode, AdjList[MAX_VERTEX_NUM];
typedef struct{
	AdjList vertices;
	int vexnum,arcnum;         // 图的当前顶点数和弧数
	int kind;                  // 图的种类标志
}ALGraph;

若无向图中有n个顶点、e条边,则它的邻接表需n个头结点和2e个表结点,在边稀疏的情况下(e≤n(n-1)/2),用邻接表表示图比邻接矩阵节省存储空间,当和边相关的信息较多时更是如此。
在无向图中的邻接表中,顶点vi的度恰为第i个链表中的结点数,而在有向图中,第i个链表中的结点个数只是顶点vi的出度,为求入度,必须遍历整个邻接表。在所有链表中其邻接点域的值为i的结点的个数是顶点vi的入度。有时,为了便于确定顶点的入度或以顶点vi为头的弧,可以建立一个有向图的逆邻接表,即对每个顶点vi建立一个链接以vi为头的弧的表。
在建立邻接表或逆邻接表时,若输入的顶点信息即为顶点的编号,则建立邻接表的时间复杂度为O(n+e),否则,需要通过查找才能得到顶点在图中位置,则时间复杂度为O(n×e)。
在邻接表上容易找到任一顶点的第一个邻接点和下一个邻接点,但要判定任意两个顶点(vi和vj)之间是否有边或弧相连,则需搜索第i个或第j个链表,不及邻接矩阵方便。

7.2.3 十字链表

十字链表是有向图的另一种链式存储结构。可以看成是将有向图的邻接表和逆邻接表结合起来得到的一种链表。在十字链表中,对应于有向图中每一条弧有一个结点,对应于每个结点也有一个结点。
弧结点:
tailvex headvex hlink tlink info
顶点结点:
data firstin firstout
在弧结点中有5个域,其中尾域(tailvex)和头域(headvex)分别指示弧尾和弧头这两个顶点在图中的位置,链域hlink指向弧头相同的下一条弧,而链域tlink指向弧尾相同的下一条弧,info域指向该弧的相关信息。弧头相同的弧在同一个链表中,弧尾相同的弧也在一个链表上。它们的头结点即为顶点结点,它由3个域组成:其中data域存储和顶点相关的信息,如顶点的名称等;firstin和firstout为两个链域,分别指向以该顶点为弧头或弧尾的第一个弧结点。
若将有向图的邻接矩阵看成是稀疏矩阵的话,则十字链表也可以看成是邻接矩阵的链表存储结构,在图的十字链表中,弧结点所在的链表非循环链表,结点之间相对位置自然形成,不一定按顶点序号有序,表头结点即顶点结点,它们之间不是链接,而是顺序存储。
有向图的十字链表存储表示的形式说明如下所示:

// define MAX_VERTEX_NUM 20
typedef struct ArcBox{
	int    tailvex,headvex;          // 该弧的尾和头顶点的位置
	struct  ArcBox *hlink, *tlink;   // 分别为弧头相同和弧尾相同的弧的链域
	InfoType *info;                  // 该弧相关信息的指针
}ArcBox;

typedef struct VexNode{
	VertexType data;
	ArcBox *firstin, *firstout;      // 分别指向该顶点第一条入弧和出弧
}VexNode;

typedef struct{
	VexNode xlist[MAX_VERTEX_NUM];   // 表头向量
	int vexnum,arcnum;               // 有向图的当前顶点数和弧数 
}OLGraph;

只要输入n个结点的信息和e条弧的信息,便可建立该有向图的十字链表,其算法如下:

Status CreateDG(OLGraph &G){
	// 采用十字链表存储表示,构造有向图G(G.kind = DG)
	scanf(&G.vexnum, &G.arcnum, &IncInfo)        // IncInfo为0则弧不含其它信息
	for(i =0; i<G.vexnum; ++i){                  // 构造表头向量
		scanf(&G.xlist[i].data);                 // 输入顶点值
		G.xlist[i].firstin = NULL; G.xlist[i].firstout = NULL;  // 初始化指针
	}
	for(k=0; k<G.arcnum; ++k){                     // 输入各弧并构造十字链表
		scanf(&v1,&v2);                            // 输入一条弧的始点和终点
		i = LocateVex(G,v1); j = LocateVex(G,v2);  // 确定v1和v2在G中位置
		p = (ArcBox *)malloc(sizeof(ArcBox));      // 假设有足够空间
		*p = {i,j,G.xlist[j].firstin,G.xlist[i].firstout,NULL} // 对弧结点赋值{tailvex,headvex,hlink,tlink,info}
		G.xlist[j].firstin = G.xlist[i].firstout = p; // 完成在入弧和出弧链头的插入
		if(IncInfo) Input(*p->info);                  // 若弧含有相关信息,则输入
	}
}//CreateDG

在十字链表中既容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得顶点的出度和入度,或需要,可在建立十字链表的同时求出。同时,上述算法建立十字链表的时间复杂度和建立邻接表时相同的。在某些有向图的应用中,十字链表是很有用的工具。

7.2.4 邻接多重表

邻接多重表是无向图的另一种链式存储结构。虽然邻接表时无向图的一种很有效的存储结构,在邻接表中容易求得顶点和边的各种信息。但是,在邻接表中的每一条边(vi,vj)有两个结点,分别在第i个和第j个链表中,这给某些图的操作带来不便。
邻接多重表的结构和十字链表类似。在邻接多重表中,每一条边用一个结点表示,它由如下所示的6个域组成:
mark ivex ilink jvex jlink info
其中mark为标志域,可用以标记该条边是否被搜索过;ivex和jvex为该边依附的两个顶点在图中的位置;ilink指向下一条依附于顶点ivex的边;jlink指向下一条依附于顶点jvex的边,info为指向和边相关的各种信息的指针域。
每一个顶点也用一个顶点表示,它由如下所示的两个域组成:
data firstedge
其中data域存储和该顶点相关的信息,firstedge域指示第一条依附于该顶点的边。
在邻接多重链表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,则每个边结点同时链接在两个链表中。可见,对无向图而言,其邻接多重表和邻接表的差别,仅仅在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。因此,除了在边结点中增加一个标志域外,邻接多重表所需的存储量和邻接表相同。邻接多重表的类型说明如下:

// 无向图的邻接多重表存储表示
#define MAX_VERTEX_NUM 20
typedef enum{unvisited,visited} VisitIf;
typedef struct EBox{
	VisitIf      mark;          // 访问标记
	int     ivex,jvex;          // 该边依附的两个顶点的位置
	struct EBox *ilink, *jlink; // 分别指向依附这两个顶点的下一条边
	InfoType      *info;        // 该边信息指针
}EBox;
typedef struct VexBox{
	VertexType        data;
	EBox        *firstedge;     // 指向第一条依附该顶点的边
}VexBox;
typedef struct {
	VexBox   adjmulist[MAX_VERTEX_NUM]; 
	int                 vexnum,edgenum;    // 无向图的当前顶点数和边数
}AMLGraph;

7.3 图的遍历

图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。因为图的任一顶点都可能和其余的顶点相邻接。为了避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点。为此,我们可以设一个辅助数组visited[0…n-1],它的初始位置置为“假”或零,一旦访问了顶点vi,变置visited[i]为真或者为被访问时的次序号。
通常有两种遍历图的路径:深度优先算法和广度优先算法。它们对无向图和有向图都适用。

7.3.1 深度优先搜索

深度优先搜索(Depth_First Search)遍历类似于树的先根遍历,是树的先根遍历的推广。假设初始状态是图中所有顶点未曾被访问,则深度优先搜素可从图中某个顶点v出发,访问此顶点,然后依次从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到;若此时尚有顶点未被访问,则另选图中一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。这是一个递归的过程,为了在遍历过程中便于区分顶点是否已被访问,需附设访问标志数组visited[0…n-1],其初值为“false”,一旦某个顶点被访问,则其相应的分量置为“true”。
整个图的遍历算法如下述算法所示,其中w≥0表示存在邻接点。

Boolean visited[MAX];        // 访问标志数组
Status (*VisitFunc)(int v);  // 函数变量

void DFSTraverse(Graph G, Status(*Visit)(int v)){
	// 对图G作深度优先遍历
	VisitFunc = Visit;    // 使用全局变量VisitFunc,使DFS不必设函数指针参数
	for(v = 0; v <G.vexnum; ++v) visited[v] = FALSE;  // 访问标志数组初始化
	for(v = 0; v <G.vexnum; ++v)
		if(!visited[v])  DFS(G,v);                    // 对尚未访问的顶点调用DFS
}

void DFS(Graph G, int v){
	// 从第v个顶点出发递归地深度优先遍历图G
	visited[v] = TRUE; VisitFunc(v);     // 访问第v个顶点
	for(w = FirstAdjVex(G,V); w >=0; w = NextAdjVex(G,v,w))
		if(!visited[w]) DFS(S,G);        // 对v的尚未访问的邻接顶点w递归调用DFS
}

分析上述算法,在遍历图时,对图中每个顶点至多调用一次DFS函数,因为一旦某个顶点被标志成已被访问,就不再从它出发进行搜索。因此,遍历图的过程实质上是对每个顶点查找其邻接点的过程。其耗费的时间则取决于所采用的存储结构。当用二维数组表示邻接矩阵作图的存储结构时,查找每个顶点的邻接点所需时间为O(n2),其中n为图中的顶点数。而当以邻接表作图的存储结构时,找邻接点所需时间为O(e),其中e为无向图中边的数或有向图中弧的数。由此,当以邻接表作存储结构时,深度优先遍历图的时间复杂度为O(n+e)。

7.3.2 广度优先搜索

广度优先搜索(Broadth_First Search)遍历类似于树的按层次遍历的过程。
假设从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。换句话说,广度优先遍历图的过程是以v为起始点,由近至远,依次访问和v有路径相通且路径长度为1,2,…的顶点。
和深度优先搜索类似,在遍历的过程中也需要一个访问标志数组。并且为了顺次访问路径长度为2、3、…的顶点,需附设队列以存储已被访问的路径长度为1,2,…的顶点,广度优先遍历的算法如下:

void BFSTraverse(Graph G,Status(*Visit)(int v)){
	// 按广度优先非递归遍历图G,使用辅助队列Q和访问标志数组visited
	for(v = 0; v <G.vexnum; ++v) visited[v] = FALSE;
	InitQueue(Q);                          // 置空的辅助队列Q
	for(v = 0; v < G.vexnum; ++V)
		if(!visited[v]){
			visited[v] = TRUE;Visit(v);
			EnQueue(Q,v);                  // v入队列
			while(!QueueEmpty(Q)) {
				DeQueue(Q,u);              // 队头元素出队并置为u
				for(w = FirstAdjVex(G,u); w>=0; w= NextAdjVex(G,u,w))
					if(!Visited[w]) {      // w为u的尚未访问的邻接顶点
						Visited[w] = TRUE; Visit(w);
						EnQueue(Q,W);
					}// if
			}// while
		}// if
}// BFSTraverse

分析上述算法,每个顶点至多进一次队列。遍历图的过程实质上是通过边或弧找邻接点的过程。因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两者不同之处仅仅在于对顶点访问的顺序不同。

7.4 图的连通性问题

这一节,我们将利用遍历图的算法求解图的连通性问题,并讨论最小代价生成树以及重连接性与通信网络的经济性和可靠性的关系。

7.4.1 无向图的连通分量和生成树

在对无向图进行遍历时,对于连通图,仅需从任一顶点出发进行深度优先搜索或广度优先搜索,便可访问到图中所有结点。对非连通图,则需从多个顶点出发进行搜素,而每一次从一个新的起始点出发进行搜索过程中得到的顶点访问序列恰为其各个连通分量中的顶点集。
设E(G)为连通图G中所有边的集合,则从图中任一顶点出发遍历图时,必定将E(G)分成两个集合T(G)和B(G),其中T(G)是遍历图过程中历经的边的集合,B(G)是剩余的边的结合。显然T(G)和图G中所有顶点一起构成连通图G的极小连通子图。它是连通图的一棵生成树,并且称由深度优先搜索得到的为深度优先生成树,由广度优先搜索得到的为广度优先生成树。
对于非连通图,每个连通分量中的顶点集,和遍历时走过的边一起构成若干棵生成树,这些连通分量的生成树组成非连通图的生成森林。
假设以孩子兄弟链表作生成森林的存储结构,则下述算法为生成非连通图的深度优先生成森林,算法时间复杂度和遍历相同,其中DFSTree为下下个算法。

void DFSForest(Graph G, CSTree &T){
	// 建立无向图G的深度优先生成森林的(最左)孩子(右)兄弟链表T
	T = NULL;
	for(v = 0; v <G.vexnum; ++v)
		visited[v] = FALSE;
	for(v = 0; v <G.vexnum; ++v)
		if(!visited[v]){                        // 第v顶点为新的生成树的根节点
			p = (CSTree)malloc(sizeof(CSNode)); // 分配根节点
			* p = {GetVex(G,v), NULL,NULL};     // 给该结点赋值
			if(!T) T= p;                        // 是第一棵生成树的根(T的根)  
			else q->nextsibling = p;            // 是其他生成树的根(前一棵的根的“兄弟”)
			q = p;                              // q指示当前生成树的根
			DFSTree(G,v,p);                     // 建立以p为根的生成树
 		}	
}// DFSForest

void DFSTree(Graph G, int v, CSTree &T){
	// 从第v个顶点出发深度优先遍历图G,建立以T为根的生成树
	visited[v] = TRUE; first = TRUE;
	for(w = FirstAdjVex(G,v); w >= 0; w = NextAdjVex(G,v,w))
		if(!visited[w]){
			p = (CSTree)malloc(sizeof(CSNode));   // 分配孩子结点
			*p= {GetVex(G,w),NULL,NULL};
			if(first){                         // w是v的第一个未被访问的邻接顶点
				T->lchild = p; first = FALSE;  // 是根的左孩子结点
			}// if
			else{                              // w是v的其他未被访问的邻接顶点
				q->nextsibling = p;            // 是上一邻接顶点的左兄弟结点
			}// else
			q = p;
			DFSTree(G,w,q);                    // 是第w个顶点出发深度优先遍历图G,建立子生成树q
		}// if
}// DFSTree

7.4.3 最小生成树

现在我们要选择这样一颗生成树,也就是总的耗费最少。这个问题就是构造连通网的最小代价生成树的问题,简称为最小生成树。一颗生成树的代价就是树上各边的代价之和。构造最小生成树可以有多种算法,其中多数算法利用了最小生成树的下列一种简称为MST的性质:假设N=(V,{E})是一个连通网,U是顶点集V的一个非空子集,若(u,v)是一条具有最小权值(代价)的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树。
普里姆(Prim)算法和克鲁斯科尔(Kruskal)算法是两个利用MST性质构造最小生成树的算法。
下面先介绍普里姆算法:
假设N=(V,{E})是连通网,TE是N上最小生成树中边的集合。算法从U={u0}(u0∈V),TE={}开始,重复执行下述操作:在所有u∈U,v∈V-U的边(u,v)∈E中找一条代价最小的边(u0,v0)并入集合TE,同时v0并入U,直到U= V为止,此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树。
为了实现这个算法需附设一个辅助数组closedge,以记录从U到V-U具有最小代价的边。对每个顶点vi∈V-U,在辅助数组中存在一个相应分量closedge[i-1],它包括两个域,其中lowcost存储该边上的权。显然有:
closedge[i-1] .lowcost = Min{cost(u,vi)|u∈U}
vex域存储该边依附的在U中的顶点。

void MiniSpanTree_PRIM(MGraph G, VertexType u){
	// 用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边
	// 记录从顶点集U到V-U的代价最小的边的辅助数组定义:
	// struct{
	//     VertexType    adjvex;
	//     VRType        lowcost;
	// }// closedge[MAX_VERTEX_NUM];
	k = LocateVex(G,u);
	for(j = 0; j < G.vexnum; ++j)    // 辅助数组初始化
		if(j!= k) closedge[j] = {u,G.arcs[k][j].adj};  // {adjvex, lowcost}
	closedge[k].lowcost = 0;         // 初始,U={u}
	for(i = 1; i < G.vexnum; ++i){   // 选择其余G.vexnum-1个顶点
		k = mininum(closedge);       // 求出T的下一个结点:第k顶点
		// 此时closedge[k].lowcost =           MIN{closege[vi].lowcost|closedge[vi].lowcost>0,vi∈V-U}
		printf(closedge[k].adjvex,G.vexs[k]);   // 第k顶点并入U集
		for(j = 0; j < G.vexnum; ++j)
			if(G.arcs[k][j].adj<closedge[j].lowcost)   // 新顶点并入U后重新选择最小边
				closedge[j] = {G.vexs[k],G.arcs[k][j].adj};
	}
}// MiniSpanTree

普里姆算法的时间复杂度为O(n2),因此适用于求边稠密的网的最小生成树。而克鲁斯卡尔算法的时间复杂度为O(eloge),e为网中边的数目,适用于求边稀疏的网的最小生成树。该算法从另一途径求网的最小生成树:假设连通网N=(V,{E}),则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。

7.5 有向无环图及其应用

一个无环的有向图称做有向无环图(directed acycline graph),简称DAG图。DAG是一类较有向树更一般的特殊有向图,有向无环图是描述含有公共子式的表达式的有效工具。若利用有向无环图,则可实现对相同子式的共享,从而节省存储空间。
检查一个有向图是否存在环要比无向图复杂。对于无向图来说,若深度优先遍历过程中遇到回边,即指向已访问过的顶点的边,则必定存在环;而对于有向图来说,这条回边有可能是指向深度优先森林中另一棵生成树上顶点的弧。但是,如果从有向图上某个顶点v出发的遍历,在dfs(v)结束之前出现一条从顶点u到顶点v的回边,由于u在生成树上是v的子孙,则有向图中必定存在包含顶点v和u的环。
有效无环图也是描述一项工程或系统的进行过程的有效工具。几乎所有的工程都可以分为若干个称做活动的子工程,而这些子工程之间,通常受着一定条件的约束,如其中某些子工程开始的开始必须在另一些子工程完成之后。对整个工程和系统,人们关系的是两个方面的问题:一是工程能否顺利进行;二是估算整个工程完成所必须的最短时间,对应于有向图,即为进行拓扑排序和求关键路径的操作。

7.5.1 拓扑排序

拓扑排序简单来说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。离散数学上关于偏序和全序的定义:
若集合X上的关系R是自反的、反对称的和传递的,则称R是集合X上的偏序关系。设R是集合X上的偏序,如果对于每个x,y∈X,则必有xRy或yRx,则称R是集合X上的全序关系。直观地看,偏序指集合中的仅有部分成员之间可比较,而全序指集合中全体成员之间均可比较。由偏序定义得到拓扑排序的操作便是拓扑排序。
这种用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网,简称AOV-网。在网中,若从顶点i到顶点j有一条有向路径,则i是j的前驱,j是i的后继。若<i,j>是网中一条弧,则i是j的直接前驱,j是i的直接后继。
因此,对给定的AOV-网应首先判定网中是否存在环。检测的方法是对有向图构造其顶点的拓扑有序排列,若网中所有顶点都在它的拓扑有序排列中,则该AOV-网中必定不存在环。
如何进行拓扑排序?解决的方法很简单:
(1)在有向图中选一个没有前驱的顶点且输出之。
(2) 从图中删除该顶点和所有以它为尾的弧。
重复上述两步,直至全部顶点均已输出,或者当前图中不存在无前驱的顶点为止。后一种情况说明有向图中存在环。
针对上述两步操作,我们可采用邻接表作有向图的存储结构,且在头结点中增加一个存放顶点入度的数组。入度为零的顶点即为没有前驱的顶点,删除顶点及以它为尾的弧的操作,则可换以弧头顶点的入度减1来实现。
为了避免重复检测入度为零的顶点,则可设一栈暂存所有入度为零的顶点,由此可得拓扑排序的算法如下所示:

Status TopologicalSort(ALGraph G){
	// 有向图G采用邻接表存储结构
	// 若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则ERROR
	FindInDegree(G, indegree);         // 对各顶点求入度indegree[0..vernum-1]
	InitStack(S);
	for(i=0; i < G.vexnum; ++i)        // 建零入度顶点栈S
		if(!indegree[i]) Push(S,i);    // 入度为0者进栈
	count = 0;                         // 对输出顶点进栈
	while(!StackEmpty(S)){
		Pop(S,i); printf(i,G.vertices[i].data); ++count;    // 输出i号订单并计数
		for(p = G.vertices[i].firstarc; p; p = p->nextarc){
			k = p->adjvex;                     // 对i号顶点的每个邻接点的入度减1
			if(!(--indegree[k])) Push(S,k);    // 若入度减为0,则入栈        
		}// for
	}// while
	if(count < G.vexnum) return ERROR;         // 该有向图有回路
	else return OK;
}// TopologicalSort

分析上述算法,对有n个顶点和e条弧的有向图而言,建立求各顶点的入度的时间复杂度为O(e),建零入度顶点栈的时间复杂度为O(n),在拓扑排序过程中,若有向图无环,则每个顶点进一次栈,出一次栈,入度减1的操作在WHILE语句中总共执行e次,所有,总的时间复杂度O(n+e)。上述拓扑排序的算法亦是下节讨论的求关键路径的基础。当有向图中无环时,也可利用深度优先遍历进行拓扑排序,因为图中无环,则由图中某点出发进行深度优先搜索遍历时,最先退出DFS函数的顶点即出度为零的顶点,是拓扑有序序列中最后一个顶点。由此,按退出DFS函数的先后记录下来的顶点序列(如同求强连通分量时finished数组中的顶点序列)即为逆向的拓扑有序序列。

7.5.2 关键路径

与AOV-网相对应的是AOE-网(Activity On Edge),即边表示活动的网。AOE-网是一个带权的有向无环网,其中顶点表示事件,弧表示活动,权表示活动持续的时间。通常,AOE-网可用来估算工程的完成时间。
由于整个工程只有一个开始点和一个完成点,故在正常的情况下(无环)下,网中只有一个入度为零的点,称为源点,和一个出度为零的点,叫做汇点。
和AOV-网不同,对AOE-网有待研究的问题时:
(1)完成整项工程至少需要多少时间?
(2)哪些活动是影响工程进度的关键?
由于在AOE-网中有些活动可以并行地进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度,这里路径长度是指路径上各活动持续时间之和,而不是路径上弧的数目。路径长度最长的路径叫做关键路径。假设开始点是v1,从v1到v~i ~的最长路径长度叫做vi的最早发生时间。这个时间决定了所有以vi为尾的弧所表示的活动的最早开始时间。我们用e(i)表示活动ai的最早开始时间。
还可以定义一个活动的最迟开始时间l(i),这是在不推迟整个工程完成的前提下,活动ai最迟必须开始进行的时间。两者之差表示完成活动ai的时间余量。我们把l(i)=e(i)的活动叫做关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程的进度。
分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的功效,缩短整个工期。
关键活动的速度提高是有限度的,只有在不改变网的关键路径的情况下,提高关键活动的速度才有效。另一方面,若网中有几条关键路径,那么单是提高一条关键路径上的速度,还不能导致整个工程缩短工期,而必须提高同时在几个关键路径上的活动的速度。

7.6 最短路径

本节将讨论带权有向图,并称路径上的第一个顶点为源点,最后一个顶点为终点。下面讨论两个最常见的最短路径问题。

7.6.1 从某个源点到其余各顶点的最短路径

我们先来讨论单源点的最短路径问题:给定带权有向图G和源点v,求从v到G中其余各顶点的最短路径。如何求得这些路径?Dijkstra提出了一个按路径长度递增的次序产生最短路径的算法:
首先,引入一个辅助向量D,它的每个分量D[i]表示当前所找到的从始点v到每个终点vi的最短路径的长度。它的初态为:若从v到vi有弧,则D[i]为弧上的权值,否者置D[i]为∞,显然长度为
D[j] = Mini{ D[i] | vi∈V }
的路径就是从v出发的长度最短的一条路径。此路径为(v,vj)。
那么下一条长度次短的最短路径是哪一条呢?假设该次短路径的终点是vk,则可想而知,这条路径或者是(v,vk),或者是(v,vj,vk)。它的长度或者是从v到vk的弧上的权值,或者是D[j]和从vj到vk的弧上的权值之和。一般情况下,假设S为已求得最短路径的终点的集合,则可证明:下一条最短路径(设其终点为x)或者是弧(v,x),或者是中间只经过S中的顶点而最后到达顶点x的路径。
因此,在一般情况下,下一条长度次短的最短路径的长度必是:
D[j] = Mini{ D[i] | vi∈V-S }
其中,D[i]或者是弧(v,vi)的权值,或者是D[k],vk∈S,和弧(vk,vi)上的权值之和。

7.6.2 每一对顶点之间的最短路径

解决这个问题的一个方法是:每次以一个顶点为源点,重复执行Dijkstra算法n次,便可求得每一对顶点之间的最短路径。总的执行时间为O(n3)。这里介绍Floyd提出的另一个算法,这个算法的时间复杂度也是O(n3),但形式上简单些。
该算法仍从图的带权邻接矩阵cost出发,其基本思想是:
假设求从顶点vi到vj的最短路径。如果从vi到vj有弧,则从vi到vj存在一条长度为arc[i][j]的路径,该路径不一定是最短路径,尚需进行n次试探。首先考虑路径(vi,v0,vj)是否存在,即判定(vi,v0)和(v0,vj)是否存在。如果存在,则比较(vi,vj)和(vi,v0,vj)的路径长度取长度最短者为从vi到vj的中间顶点的序号不大于0的最短路径。假设在路径在增加一个顶点v1,也就是说,如果(vi,…,v1)和(v1,…,vj)分别是当前找到的中间顶点的序号不大于0的最短路径,那么(vi,…,v1,…,vj)就有可能是从vi到vj的中间顶点的序号不大于1的最短路径。将它和已经得到的从vi到vj的中间顶点的序号不大于0的最短路径相比较,从中选出中间顶点的序号不大于1的最短路径之后,再增加一个顶点v2,继续进行试探。依次类推。这样在经过n次比较后,最后求得的必是从vi到vj的最短路径。按此方法,可以同时求得各对顶点间的最短路径。

标签:结点,有向图,vi,路径,C语言,严蔚敏,邻接,顶点,数据结构
来源: https://blog.csdn.net/qq_40844276/article/details/113878034

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有