数据结构(19)--DAG应用之AOE网的拓扑排序

参考书籍:数据结构(C语言版)严蔚敏吴伟民编著清华大学出版社

本文中的代码可从这里下载:https://github.com/qingyujean/data-structure

1.有向无环图

    有向无环图(directed acycline graph)简称DAG图,是描述一项工程或系统的进行过程的有效工具。对整个工程和系统,人们关心的是两个方面的问题:一是工程能否顺利进行;二是估算整个工程完成所必须的最短时间。
    有向无环图的应用:1.拓扑排序;2.关键路径
    在工程实践中,一个工程项目往往由若干个子项目组成,这些子项目间往往有多种关系:
    1.先后关系,即必须在一子项目完成后,才能开始实施另一个子项目;
    2.子项目之间无次序要求,即两个子项目可以同时进行,互不影响。

2.拓扑排序

    我们用一种有向图来表示上述问题。在这种有向图中,顶点表示活动,有向边表示活动的优先关系,这种有向图叫做顶点表示活动的网络(Activity On Vertex Network)简称为AOV网
    在AOV网络中,如果顶点Vi的活动必须在顶点Vj的活动以前进行,则称Vi为Vj的前趋顶点,而称Vj为Vi的后继顶点。这种前趋后继关系有传递性。此外,任何活动i不能以它自己作为自己的前驱或后继,这叫做反自反性
    从前驱和后继的传递性和反自反性来看,AOV网中不能出现回路(有向环),回路表示顶点之间的先后关系进入了死循环。
    判断AOV网是否有有向环的方法是对该AOV网进行拓扑排序,将AOV网中顶点排列成一个线性有序序列,若该线性序列中包含AOV网全部顶点,则AOV网无环,否则,AOV网中存在有向环,该AOV网所代表的工程是不可行的。

    何谓“拓扑排序” ?
拓扑序列:
    在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列。
拓扑排序:
    由AOV网构造拓扑序列的过程叫拓扑排序。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称为它的拓扑序列。

    如何进行拓扑排序?
    解决方法:
  1)从有向图中选取一个没有前驱的顶点,并输出之;
  2)从有向图中删去此顶点以及所有以它为尾的弧;
  3)重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。后一种情况说明有向图中存在环。

    拓扑排序算法的实现
    1.为了实现拓扑排序的算法,对给定的有向图可采用邻接表作为它的存储结构。
    2.将每个链表的表头结点构成一个顺序表,各表头结点的Data域存放相应顶点的入度值。每个顶点入度初值可随邻接表动态生成过程中累计得到。
    3.在拓扑排序过程中,凡入度为零的顶点即是没有前趋的顶点,可将其取出列入有序序列中,同时将该顶点从图中删除掉不再考虑。
删去一个顶点时,所有它的直接后继顶点入度均减1,表示相应的有向边也被删除掉。
    4.设置一个堆栈,将已检验到的入度为零的顶点标号进栈,当再出现新的无前趋顶点时,也陆续将其进栈。每次选入度为零的顶点时,只要取栈顶顶点即可。

    用邻接表存储AOV网络,拓扑排序算法描述:
(1) 把邻接表中所有入度为零的顶点进栈;
(2) 在栈不空时:
    ① 退栈并输出栈顶的顶点 j;
    ② 在邻接表的第 j 个单链表中,查找顶点为 j 的所有直接后继顶点 k,并将 k 的入度减1。若顶点 k 的入度为零,则顶点 k 进栈;
    ③ 若栈空时输出的顶点个数不是 n,则有向图中有环路,否则拓扑排序完毕。

示例:

3.代码实现

3.1定义

 

/*
DAG 有向无环图的应用--拓扑排序:能否顺利完成工程,即检查是否存在环,
AOV网:顶点表示活动的网

除了拓扑排序检查环以外,还可以用DFS
当有向图中无环时,从图中某点进行深度优先遍历时,最先退出DFS函数的顶点即出度为0的顶点,是拓扑序列中的最后一个顶点,由此,按退出DFS函数的先后记录下来的顶点序列,即为逆向的拓扑有序序列
*/

//本次示例采用邻接表作为有向图的存储结构


#include<stdio.h>
#include<stdlib.h>
/*
图的表示方法
DG(有向图)或者DN(有向网):邻接矩阵、邻接表(逆邻接表--为求入度)、十字链表
UDG(无向图)或者UDN(无向网):邻接矩阵、邻接表、邻接多重表
*/
#define MAX_VERTEX_NUM 10//最大顶点数目
#define NULL 0
typedef int VRType;//对于带权图或网,则为相应权值
typedef int VertexType;//顶点类型
//typedef enum GraphKind {DG, DN, UDG, UDN};  //有向图:0,有向网:1,无向图:2,无向

typedef struct ArcNode{	
	int adjvex;//该弧所指向的顶点的在图中位置
	//VRType w;//弧的相应权值
	struct ArcNode *nextarc;//指向下一条弧的指针
}ArcNode;//弧结点信息

typedef struct VNode{
	VertexType data;//顶点信息

	ArcNode *firstarc;//指向第一条依附该顶点的弧的指针
}VNode, AdjVexList[MAX_VERTEX_NUM];//顶点结点信息

typedef struct{
	AdjVexList vexs;//顶点向量
	int vexnum, arcnum;//图的当前顶点数和弧数
	//GraphKind kind;//图的种类标志
}ALGraph;//邻接表表示的图
#define OK 1
#define ERROR 0
typedef int status;

static int indegree[MAX_VERTEX_NUM] = {0};//存放各个顶点的入度的数组

 

3.2邻接表表示有向无环图

 

 

//若图G中存在顶点v,则返回v在图中的位置信息,否则返回其他信息
int locateVex(ALGraph G, VertexType v){
	for(int i = 0; i < G.vexnum; i++){
		if(G.vexs[i].data == v)
			return i;
	}
	return -1;//图中没有该顶点
}


//采用邻接表表示法构造有向图G
void createDG(ALGraph &G){
	printf("输入顶点数和弧数如:(5,3):");
	scanf("%d,%d", &G.vexnum, &G.arcnum);

	//构造顶点向量,并初始化
	printf("输入%d个顶点(以空格隔开如:v1 v2 v3):", G.vexnum);
	getchar();//吃掉换行符
	for(int m = 0; m < G.vexnum; m++){
		scanf("v%d", &G.vexs[m].data);
		G.vexs[m].firstarc = NULL;//初始化为空指针////////////////重要!!!
		getchar();//吃掉空格符
	}

	//构造邻接表
	VertexType v1, v2;//分别是一条弧的弧尾和弧头(起点和终点)
	//VRType w;//对于无权图或网,用0或1表示相邻否;对于带权图或网,则为相应权值	
	printf("\n每行输入一条弧依附的顶点(先弧尾后弧头)如:v1v2:\n");
	fflush(stdin);//清除残余后,后面再读入时不会出错
	int i = 0, j = 0;
	for(int k = 0; k < G.arcnum; k++){
		scanf("v%dv%d",&v1, &v2);
		fflush(stdin);//清除残余后,后面再读入时不会出错
		i = locateVex(G, v1);//弧起点
		j = locateVex(G, v2);//弧终点
		
		//采用“头插法”在各个顶点的弧链头部插入弧结点
		ArcNode *p1 = (ArcNode *)malloc(sizeof(ArcNode));//构造一个弧结点,作为弧vivj的弧头(终点)
		p1->adjvex = j;
		//p1->w = w;
		p1->nextarc = G.vexs[i].firstarc;
		G.vexs[i].firstarc = p1;
		/*因为是有向图,所以不必创建2个弧结点
		ArcNode *p2 = (ArcNode *)malloc(sizeof(ArcNode));//构造一个弧结点,作为弧vivj的弧尾(起点)
		p2->adjvex = i;
		//p2->w = w;
		p2->nextarc = G.vexs[j].firstarc;
		G.vexs[j].firstarc = p2;
		*/
	}
}

 

3.3拓扑排序的实现

 

void findInDegree(ALGraph G, int indegree[]){	
	ArcNode *p;
	for(int i = 0; i < G.vexnum; i++){
		for(p = G.vexs[i].firstarc; p; p = p->nextarc){
			indegree[p->adjvex]++;
		}
	}		
}

//如有向图无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR
status toplogicalSort(ALGraph G){
	//先初始化各个顶点的入度
	findInDegree(G, indegree);
	

	int stack[MAX_VERTEX_NUM];//维护一个栈来存放入度为0的顶点,当栈为空时,则说明图中不存在无前驱的顶点了(即没有入度为0的顶点了),说明图中无环
	//否则如果此时仍然存在顶点,而且这些顶点有前驱,则说明有环
	int top = 0;//栈顶指针

	//将入度为0的顶点入栈
	for(int i = 0; i < G.vexnum; i++){
		if(!indegree[i]){
			stack[top++] = i;
		}
	}

	int count = 0;//对输出的顶点计数
	ArcNode *p;
	while(top != 0){//栈不为空
		int topElemVex_i = stack[--top];//栈顶元素出栈,即第一个无前驱的顶点
		printf("v%d ", G.vexs[topElemVex_i].data);//输出当前结点
		count++;

		//去掉以该结点为前驱的点与他的弧,以将相关顶点的入度减1的操作来实现
		for(p = G.vexs[topElemVex_i].firstarc; p; p = p->nextarc){
			indegree[p->adjvex]--;
			if(!indegree[p->adjvex]){
				stack[top++] = p->adjvex;//入度为0者入栈
			}
		}
	}
	printf("\n");

	if(count < G.vexnum)//该有向图有回路
		return ERROR;
	else
		return OK;
}

 

3.4演示

 

 

/*测试:
6,8
v1 v2 v3 v4 v5 v6

v1v2
v1v3
v1v4

v3v2

v4v3
v4v5

v6v4
v6v5
*/
void main(){
	ALGraph G;
	createDG(G);
	//printAdjList(G);
	printf("该图的拓扑排序序列为:");
	toplogicalSort(G);		
}

 

分析:

    对有 n 个顶点和 e 条弧的有向图而言,建立求各顶点的入度的时间复杂度为O(e);建零入度顶点栈的时间复杂度为O(n);在拓扑排序过程中,若有向图无环,则每个顶点进一次栈,出一次栈,入度减1的操作在 WHILE语句中总共执行e次,所以,总的时间复杂度为O(n+e)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章