数据结构—图的操作与应用—第11关:求AOE网(邻接表存储)的关键路径算法


任务描述

本关任务:AOE网的存储结构为邻接表,要求编写函数实现AOE网的关键路径算法。

相关知识

关键路径基本概念

用带权有向图描述工程的预计进度,以顶点表示事件,有向边表示活动,边上的权值表示完成活动所需的时间(比如天数),或者说活动持续时间。

例如,某施工项目主要由a1a11共11个任务构成,各个任务的含义及其开始的先后次序表示如表所示:

代号工序名称工时(天数)先前工序
a1清理现场6
a2准备材料4
a3地面施工5
a4预制墙及房顶桁架1a1
a5混凝土地面保养1a2
a6立墙架2a3
a7装天花板9a4,a5
a8油漆7a4,a5
a9引道混凝土施工4a6
a10保养清理现场2a7
a11交工验收11a8,a9

这些任务之间的先后次序用上表表示不直观,可以用图1表示出来。

用顶点表示事件,用有向边表示活动,有向边的权值表示活动所需要的时间,用这种方法构造的有向无环图叫作边表示活动的网(Activity On Edge Network),简称AOE网

AOE网示意图

AOE网可以回答下列问题:

  1. 完成整个工程至少需要多少时间?
  2. 为缩短完成工程所需的时间, 应当加快哪些活动?

图中入度为0的顶点v1表示工程的开始事件(如开工仪式),称为源点;出度为0的顶点v9表示工程结束事件,称为汇点

源点汇点的最长路径的长度即为完成整个工程任务所需的时间,该路径称为关键路径。关键路径上的活动称为关键活动

源点汇点的路径可能不止一条,只有各条路径上所有活动都完成了,整个工程才算完成。

因此,完成整个工程所需的最短时间取决于源点到汇点的最长路径长度,即这条路径上所有活动的持续时间之和。这条路径长度最长的路径就叫做关键路径

找出关键活动的意义在于,可以适当地增加对关键活动的投资(人力、物力等),相应地减少对非关键活动的投资,从而减少关键活动的持续时间,缩短整个工程的工期。

关键路径算法思想

由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为始点到终点的最大路径长度。关键路径长度是整个工程所需的最短工期。

要找出关键路径,必须找出关键活动, 即不按期完成就会影响整个工程完成的活动。首先计算以下与关键活动有关的量: ⑴ 事件的最早发生时间ve[k] ⑵ 事件的最迟发生时间vl[k] ⑶ 活动的最早开始时间e[i] ⑷ 活动的最晚开始时间l[i] 最后计算各个活动的时间余量 l[k] - e[k],时间余量为0者即为关键活动。

  • 事件的最早发生时间ve[k]

ve[k]是指从始点开始到顶点vk的最大路径长度。这个长度决定了所有从顶点vk发出的活动能够开工的最早时间。

事件最早开始时间

图1的每个顶点的最早开始时间如下:

图1的事件最早开始时间

  • 事件的最迟发生时间vl[k]

vl[k]是指在不推迟整个工期的前提下,事件vk允许的最晚发生时间。

事件最晚开始时间

图1的每个顶点的最晚开始时间如下:

事件最晚开始时间

  • 活动的最早开始时间e[i]

若活动ai是由弧<vk , vj>表示,则活动ai的最早开始时间应等于事件vk的最早发生时间。因此,有: e[i]=ve[k]

图1的每条边的最早开始时间如下:

活动最早开始时间

  • 活动的最晚开始时间l[i]

活动ai的最晚开始时间是指,在不推迟整个工期的前提下, ai必须开始的最晚时间。若ai由弧<vk,vj>表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后。因此,有: l[i]=vl[j]-len<vk, vj>
图1的每条边的最晚开始时间如下:

活动最晚开始时间

图1的关键活动和关键路径

关键路径算法实现

关键路径算法实现过程如下:

定义全局数组,记录顶点事件最早发生时间: int ve[MAX_VERTEX_NUM];

求出AOE网的关键活动的步骤: (1)对于源点x,置ve(x) = 0。 (2)对AOE网进行拓扑排序,如发现回路,工程无法进行,退出;否则继续下一步。 (3)按顶点的拓扑序列次序依次求其余顶点vve(v)值。 定义SqStack类型的顺序栈T为零入度顶点栈,栈元素类型为int型,栈T返回有向网G的一个拓扑序列: typedef int SElemType; // 栈元素类型

不论图的存储结构为邻接矩阵还是邻接表,关键路径算法思想是一致的,只是邻接表存储的图G每个顶点的邻接点的顺序与邻接矩阵存储的图不一样。因此,在进行拓扑排序的过程中得到的顶点序列不一样。

有向网G采用邻接表存储结构,定义TopologicalOrder函数,一边对顶点进行拓扑排序,一边求各顶点事件的最早发生时间全局数组ve

 
  1. int TopologicalOrder(ALGraph G,SqStack &T)
  2. { // 有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量)。T为拓扑序列
  3. // 顶点栈,S为零入度顶点栈。若G无回路,则用栈T返回G的一个拓扑序列,且函数值为1,否则为0
  4. int i,k,count=0; // 已入栈顶点数,初值为0
  5. int indegree[MAX_VERTEX_NUM]; // 入度数组,存放各顶点当前入度数
  6. SqStack S;
  7. ArcNode *p;
  8. FindInDegree(G,indegree); // 对各顶点求入度indegree[]
  9. InitStack(S); // 初始化零入度顶点栈S
  10. printf("拓扑序列:");
  11. for(i=0;i<G.vexnum;++i) // 对所有顶点i
  12. if(!indegree[i]) // 若其入度为0
  13. Push(S,i); // 将i入零入度顶点栈S
  14. InitStack(T); // 初始化拓扑序列顶点栈
  15. for(i=0;i<G.vexnum;++i) // 初始化ve[]=0(最小值,先假定每个事件都不受其他事件约束)
  16. ve[i]=0;
  17. while(!StackEmpty(S)) // 当零入度顶点栈S不空
  18. {
  19. Pop(S,i); // 从栈S将已拓扑排序的顶点j弹出
  20. printf("%s ",G.vertices[i].data);
  21. Push(T,i); // j号顶点入逆拓扑排序栈T(栈底元素为拓扑排序的第1个元素)
  22. ++count; // 对入栈T的顶点计数
  23. for(p=G.vertices[i].firstarc;p;p=p->nextarc)
  24. { // 对i号顶点的每个邻接点
  25. k=p->data.adjvex; // 其序号为k
  26. if(--indegree[k]==0) // k的入度减1,若减为0,则将k入栈S
  27. Push(S,k);
  28. if(ve[i]+(p->data.info)>ve[k]) // (p->data.info)是<i,k>的权值
  29. ve[k]=ve[i]+(p->data.info); // 顶点k事件的最早发生时间要受其直接前驱顶点i事件的
  30. } // 最早发生时间和<i,k>的权值约束。由于i已拓扑有序,故ve[i]不再改变
  31. }
  32. if(count<G.vexnum)
  33. {
  34. printf("此有向网有回路\n");
  35. return 0;
  36. }
  37. else
  38. return 1;
  39. }

(4)对于汇点y,置vl(y) = ve(y)。 (5)按顶点的拓扑序列次序之逆序依次求其余顶点vvl(v)值。 (6)而对le的正确计算顺序就是逆拓扑顺序,可以按拓扑序列反向计算。 (7)活动ak=<vi,vj>的最早可能开始时间e[k]=ve[i]。 (8)活动ak=<vi,vj>的最迟开始时间l[k]=vl[j]-w(<vi,vj>),只要该活动的实际开始不晚于这个时间,就不会拖延整个工程的工期。 (9)当一个活动的时间富余为零时,说明该活动必须如期完成,否则会拖延这个工程的进度。所以,若d(ak)=0,即l[k]=e[k],则活动ak是关键活动。

编程要求

AOE网的存储结构为邻接表,编写函数实现图的关键路径算法:

  • int CriticalPath(ALGraph G); // G为有向网,输出G的各项关键活动

测试说明

平台会对你编写的代码进行测试:

测试输入: 1 lt9.txt

输入说明: 第一行输入1,表示输入图的类型为有向网。 第二行输入文件名,该文件里保存了图的数据信息,内容如下: 9 11 v1 v2 v3 v4 v5 v6 v7 v8 v9 v1 v2 6 v1 v3 4 v1 v4 5 v2 v5 1 v3 v5 1 v4 v6 2 v5 v7 9 v5 v8 7 v6 v8 4 v7 v9 2 v8 v9 4 第1行为图的顶点的个数n; 第2行为图的边的条数m; 第3行至第n+2行是n个顶点的数据; 第n+3行至第n+m+2行是m条边的数据;

预期输出: 有向网 9个顶点: v1 v2 v3 v4 v5 v6 v7 v8 v9 11条弧(边): v1→v4 :5 v1→v3 :4 v1→v2 :6
v2→v5 :1
v3→v5 :1
v4→v6 :2
v5→v8 :7 v5→v7 :9
v6→v8 :4
v7→v9 :2
v8→v9 :4

拓扑序列:v1 v2 v3 v5 v7 v4 v6 v8 v9 i ve[i] vl[i] 0 0 0 关键路径经过的顶点 1 6 6 关键路径经过的顶点 2 4 6
3 5 8
4 7 7 关键路径经过的顶点 5 7 10
6 16 16 关键路径经过的顶点 7 14 14 关键路径经过的顶点 8 18 18 关键路径经过的顶点 j k 权值 ee el v1→ v2 6 0 0 关键活动 v1→ v3 4 0 2
v1→ v4 5 0 3
v2→ v5 1 6 6 关键活动 v3→ v5 1 4 6
v4→ v6 2 5 8
v5→ v7 9 7 7 关键活动 v5→ v8 7 7 7 关键活动 v6→ v8 4 7 10
v7→ v9 2 16 16 关键活动 v8→ v9 4 14 14 关键活动

输出说明: 第一行输出图的类型。 第二部分起输出图的顶点和边的数据信息。 第三部分输出有向图的一个拓扑序列,若有回路,则不能继续求关键路径。 第四部分输出事件的最早开始时间和最晚开始时间。 第五部分输出活动的最早开始时间和最晚开始时间,并且标记关键活动。

#include<stdio.h> 
#include<stdlib.h> 
#include<string.h>
#include<limits.h>  

#include"ALGraph.h"

#include"sqstack.h"

void FindInDegree(ALGraph G,int indegree[]); // 求顶点的入度

int TopologicalOrder(ALGraph G,SqStack &T); // 有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量),若G无回路,则用栈T返回G的一个拓扑序列,且函数值为1,否则为0

int CriticalPath(ALGraph G); // G为有向网,输出G的各项关键活动	int ve[MAX_VERTEX_NUM];

int ve[MAX_VERTEX_NUM];  //求各顶点事件的最早发生时间ve(全局变量)

int main()
 {
   ALGraph g;
   //printf("请创建有向网\n");
   CreateGraphF(g); // 构造有向网
   Display(g);      // 输出有向网
   CriticalPath(g); // 求g的关键路径
   return 0;
 }


void FindInDegree(ALGraph G,int indegree[])
{ // 求顶点的入度,
	int i;
	ArcNode *p;
	for(i=0;i<G.vexnum;i++)
		indegree[i]=0;     // 赋初值 
	for(i=0;i<G.vexnum;i++)
	{
		p=G.vertices[i].firstarc;
		while(p)
		{
			indegree[p->data.adjvex]++;
			p=p->nextarc;
		}
	}
}

 int TopologicalOrder(ALGraph G,SqStack &T)
 { // 有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量)。T为拓扑序列
   // 顶点栈,S为零入度顶点栈。若G无回路,则用栈T返回G的一个拓扑序列,且函数值为1,否则为0
   int i,k,count=0; // 已入栈顶点数,初值为0
   int indegree[MAX_VERTEX_NUM]; // 入度数组,存放各顶点当前入度数
   SqStack S;
   ArcNode *p;
   FindInDegree(G,indegree); // 对各顶点求入度indegree[]
   InitStack(S);             // 初始化零入度顶点栈S
   printf("拓扑序列:");
   for(i=0;i<G.vexnum;++i)  // 对所有顶点i
     if(!indegree[i])       // 若其入度为0
       Push(S,i);           // 将i入零入度顶点栈S
   InitStack(T);            // 初始化拓扑序列顶点栈
   for(i=0;i<G.vexnum;++i)  // 初始化ve[]=0(最小值,先假定每个事件都不受其他事件约束)
     ve[i]=0;
   while(!StackEmpty(S))   // 当零入度顶点栈S不空
   {
     Pop(S,i);            // 从栈S将已拓扑排序的顶点j弹出
     printf("%s ",G.vertices[i].data);
     Push(T,i);           // j号顶点入逆拓扑排序栈T(栈底元素为拓扑排序的第1个元素)
     ++count;             // 对入栈T的顶点计数
     for(p=G.vertices[i].firstarc;p;p=p->nextarc)
     {                   // 对i号顶点的每个邻接点
       k=p->data.adjvex;  // 其序号为k
       if(--indegree[k]==0) // k的入度减1,若减为0,则将k入栈S
	 Push(S,k);
       if(ve[i]+(p->data.info)>ve[k]) // (p->data.info)是<i,k>的权值
	      ve[k]=ve[i]+(p->data.info); // 顶点k事件的最早发生时间要受其直接前驱顶点i事件的
     }                                // 最早发生时间和<i,k>的权值约束。由于i已拓扑有序,故ve[i]不再改变
   }
   if(count<G.vexnum)
   {
     printf("此有向网有回路\n");
     return 0;
   }
   else
     return 1;
 }



int CriticalPath(ALGraph G)
{ 
	// G为有向网,输出G的各项关键活动
	/********** Begin **********/    
	int v1[MAX_VERTEX_NUM];
    SqStack T;
    int i,j,k,ee,el,dut;
    ArcNode *p;
    if(!TopologicalOrder(G,T))
      return 0;
    j-ve[0];
    for(i=1;i<G.vexnum;++i)
      if(ve[i]>j)
      j=ve[i];
      for(i=0;i<G.vexnum;++i)
        v1[i]=j;
      while(!StackEmpty(T))
      for(Pop(T,j),p=G.vertices[j].firstarc;p;p=p->nextarc){
        k=p->data.adjvex;
        dut=(p->data.info);
        if(v1[k]-dut<v1[j])
        v1[j]=v1[k]-dut;
      }
    printf("\ni\tve[i]\tvl[i]\n");
    for(i=0;i<G.vexnum;++i){
      printf("%d\t%d\t%d\t",i,ve[i],v1[i]);
      if(ve[i]==v1[i])
      printf(" 关键路径经过的顶点");
      printf("\n");
  }                                                                                  
  printf("j\tk\t权值\tee\tel\n");
  for(j=0;j<G.vexnum;++j)
    for(p=G.vertices[j].firstarc;p;p=p->nextarc){
      k=p->data.adjvex;
        dut=(p->data.info);
        ee=ve[j];
        el=v1[k]-dut;
        printf("%s→\t%s\t%d\t%d\t%d\t",G.vertices[j].data,G.vertices[k].data,p->data.info,ee,el);
        if(ee==el)
          printf("关键活动");
        printf("\n");
    }
  return 1;
	/********** End **********/
}

  • 38
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值