拓扑排序
什么是拓扑排序
- AOV网:在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样有向图的顶点表示活动的网,这个网叫AOV网,如下图所示
拓扑序列:设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列为V1、V2 .....Vn,若满足从顶点Vi到Vj有一条路径,且Vi在Vj之前,则这样的顶点序列为拓扑序列。
拓扑排序:求拓扑序列的过程即为拓扑排序。需要注意的是拓扑排序的结果不是唯一,如上图中所示,结果可以是C1->C2->C3->C4->C5,也可以是C2->C1->C3->C4->C5。
拓扑排序的两种结果:
- 此网中所有的顶点被输出,则说明它是不存在环的AOV网
- 如果顶点少了,说明网中存在环,不是AOV网
拓扑排序存储
- 存储使用邻接表,邻接表结构如图
其中in表示入度,data表示顶点信息,firstedge表示边表头指针,指向下一个顶点
存储结构示意图
拓扑排序代码实现
- 算法基本思路:从AOV网中选择一个入度为0的顶点开始输出,然后删除该顶点,并且删除以该顶点为尾的弧;继续重复此步骤直到输出全部顶点或AOV网中的不存在入度为0的顶点
- 核心代码实现
void CreateALGraph(MGraph G,GraphAdjList *GL) { int i,j; EdgeNode *e; //创建图 *GL = (GraphAdjList)malloc(sizeof(graphAdjList)); //对图中的顶点数.弧数赋值 (*GL)->numVertexes=G.numVertexes; (*GL)->numEdges=G.numEdges; //读入顶点信息,建立顶点表 for(i= 0;i <G.numVertexes;i++) { (*GL)->adjList[i].in=0; (*GL)->adjList[i].data=G.vexs[i]; //将边表置为空表 (*GL)->adjList[i].firstedge=NULL; } //建立边表 for(i=0;i<G.numVertexes;i++) { for(j=0;j<G.numVertexes;j++) { if (G.arc[i][j]==1) { //创建空的边表结点 e=(EdgeNode *)malloc(sizeof(EdgeNode)); //邻接序号为j e->adjvex=j; // 将当前顶点上的指向的结点指针赋值给e e->next=(*GL)->adjList[i].firstedge; //将当前顶点的指针指向e (*GL)->adjList[i].firstedge=e; (*GL)->adjList[j].in++; } } } } #pragma mark - 拓扑排序 /** 思路: 1、使用栈处理拓扑排序 2、循环链接表找到入度为0的顶点,将该顶点入栈 3、循环栈结构出栈,并打印出栈的顶点,且记录出栈顶点的个数 4、出栈后,循环遍历与出栈顶点相连接的弧,若与出栈顶点相连接的弧入度不为0则入栈 5、循环栈结构结束后判断记录出栈顶点的个数与AOV网中顶点个数是否一致,若一致则是AOV网,否则不是 复杂度:时间复杂度O(n + e),其中n为顶点的个数,e为与出栈顶点相连接弧的最多的个数 */ int AOVMap(GraphAdjList Map){ //栈顶下标 int top = 0; //记录出栈的顶点 int count = 0; //构造栈结构 int *stack = (int *)malloc(sizeof(int) * Map->numVertexes); for (int i = 0; i<Map->numVertexes; i++) { if (Map->adjList[i].in == 0) { stack[++top] = i; } } //打印入栈的顶点 printf("top = %d\n",top); //记录栈顶元素 int getTop = 0; //用来记录与出栈顶点相连接的弧的顶点 int k = 0; while (top > 0) { //出栈 getTop = stack[top]; top--; //记录顶点个数自增1 count++; //打印出栈顶点 printf("%d -> ",getTop); //遍历与顶点getTop相连接的弧 for (EdgeNode *eNode = Map->adjList[getTop].firstedge; eNode; eNode = eNode->next) { //记录相连接弧的顶点 k = eNode->adjvex; //如果相连接顶点的入度大于0,则自减1 if (Map->adjList[k].in >0) { Map->adjList[k].in--; } if (Map->adjList[k].in == 0) {//如果相连接顶点的入度等于0则入栈 stack[++top] = k; } } } printf("\n"); if (count == Map->numVertexes) {//如果最终记录的出栈顶点个数跟图的顶点个数相同则表示是AOV网 return 1; } return 0; } int main(int argc, const char * argv[]) { // insert code here... printf("Hello, World!\n"); MGraph M; GraphAdjList Map; int result; CreateMGraph(&M); CreateALGraph(M,&Map); result = AOVMap(Map); printf("result(1是AVO网,0不是AOV网):%d\n",result); return 0; }
关键路径
关键路径的定义
- AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动持续的时间,这种用有向图的边表表示活动的网,我们叫做AOE网,如图
源点、汇点:没有入边的顶点叫做源点,如上图中的V0顶点;没有出边的顶点叫终点或汇点
路径长度:路径上各个活动持续时间之和叫路径长度
关键路径:从源点到汇点具有最大的路径叫做关键路径
关键活动:关键路径上的活动叫关键活动
关键路径求解的关键参数
- 事件最早发生时间etv:即顶点Vk最早发生时间
- 事件最晚发生时间ltv:即顶点Vk最晚发生时间,如果低于这个时间开始则会导致整个工期延期的时间
- 活动最早开工时间ete:即弧Ak最早发生时间
- 活动最晚开工时间lte:即弧Ak最晚发生时间,也就是不推迟延期的最晚开工时间
关键路径存储结构
关键路径的存储结构使用AOV网的存储结构
关键路径的求解过程
- 最早开始时间etv:就是求解AOV网的过程,需要记录每个顶点的最早开工时间,当有多个权值时取最大的那个,如图所示
最晚开始时间ltv:求解ltv要从汇点(终点)开始计算,因为终点不延期其他的就不会延期。ltv求解公式为
ete、lte求解:求解图示
关键路径核心代码实现
//存储拓扑序列的栈 int *stack2; //指向stack2的栈顶下标 int top2; //最早开工以及最晚开工数组 int *etv,*ltv; #pragma mark - 进行拓扑排序 int AOVSort(GraphAdjList G) { //栈顶下标 int top = 0; //栈顶元素 int getTop; //出栈顶点个数 int count = 0; //构造栈 int *stack = (int *)malloc(sizeof(int) * G->numVertexes); //遍历链接表,将入度为0的顶点入栈 for (int i = 0; i<G->numVertexes; i++) { if (G->adjList[i].in == 0) { //注意此处入栈的是顶点在数组中的下标,不是顶点本身 stack[++top] = i; } } //初始化top2 top2 = 0; //初始化AOV序列栈 stack2 = (int *)malloc(sizeof(int) * G->numVertexes); //初始化etv etv = (int *)malloc(sizeof(int) * G->numVertexes); for (int i = 0; i<G->numVertexes; i++) { //初始化最早开始时间为0 etv[i] = 0; } //定义关联的顶点下标 int k; printf("拓扑排序:\n"); while (top>0) { //出栈并获得栈顶元素(顶点在数组中的下标) getTop = stack[top--]; //出栈计数自增1 count++; //打印出栈元素 printf("%d -> ",G->adjList[getTop].data); //将出栈的元素入栈到拓扑序列栈中 stack2[++top2] = getTop; //遍历链接表,查找与出栈相链接的顶点 for (EdgeNode *eNode = G->adjList[getTop].firstedge; eNode; eNode = eNode->next) { //获得下标 k = eNode->adjvex; if (G->adjList[k].in>0) {//相链接的顶点入度大于0则自减1 G->adjList[k].in--; } if (G->adjList[k].in == 0) {//自减1后如果等于0则入栈 stack[++top] = k; } //求相连接顶点最早开工时间 if (etv[k] < etv[getTop] + eNode->weight) { etv[k] = etv[getTop] + eNode->weight; } } } printf("\n"); printf("打印最早发生数组\n"); for (int i = 0; i<G->numVertexes; i++) { printf("etv[%d] = %d\n",i,etv[i]); } if (count == G->numVertexes) { return 1; } return 0; } #pragma mark - 求解关键路径 void AOEKeyPath(GraphAdjList G){ //进行拓扑排序并获得最早开始时间数组etv AOVSort(G); //申明最早开工时间与最晚开工时间ete、lte int ete,lte; //初始化最晚开始时间ltv ltv = (int*)malloc(sizeof(int) * G->numVertexes); for (int i = 0; i<G->numVertexes; i++) { //初始化ltv的最晚开始时间跟etv的最后一个顶点的开始时间一样 ltv[i] = etv[G->numVertexes - 1]; } //定义栈顶元素 int getTop; //定义与栈顶元素getTop相连接的顶点下标k int k; //遍历拓扑序列栈,获得ltv while (top2 > 0) { getTop = stack2[top2--]; //遍历与下标是getTop的顶点相连接的顶点 for (EdgeNode *eNode = G->adjList[getTop].firstedge; eNode; eNode = eNode->next) { //获得相连接的顶点下标 k = eNode->adjvex; //取较小的值 if (ltv[getTop] > ltv[k] - eNode->weight) { ltv[getTop] = ltv[k] - eNode->weight; } } } //打印ltv数组 printf("打印ltv数组\n"); for (int i = 0; i<G->numVertexes; i++) { printf("ltv[%d] = %d\n",i,ltv[i]); } //求解ete、lte,如果两者相等,则该顶点为关键路径上的顶点 for (int i = 0; i<G->numVertexes; i++) { for (EdgeNode *eNode = G->adjList[i].firstedge; eNode; eNode = eNode->next) { //获得与i连接的顶点下标 k = eNode->adjvex; //最早开工时间等于最早开始时间 //ete 就是表示活动 <Vk, Vi> 的最早开工时间, 是针对这条弧来说的.而这条弧的弧尾顶点Vk 的事件发生了, 它才可以发生. 因此ete = etv[k]; ete = etv[i]; //最晚开工时间等于最晚开始时间减去耗费时间 //lte 表示活动<Vk, Vi> 的最晚开工时间, 但此活动再晚也不能等Vi 事件发生才开始,而是必须在Vi事件之前发生. 所以lte = ltv[i] - len<Vk, Vi>. lte = ltv[k] - eNode->weight; if (ete == lte) { printf("<%d-%d> length = %d\n",G->adjList[i].data,G->adjList[k].data,eNode->weight); printf("关键路径顶点:-> %d \n",G->adjList[i].data); } } } printf("\n"); }