目录
1. 设计任务书... 3
1.1设计任务... 3
1.2程序功能... 3
1.3运行环境... 3
2. 本组课题... 3
2.1课题... 3
2.2本人任务... 3
3.程序功能简介... 4
3.1拓扑排序算法分析... 4
3.2关键路径算法分析... 4
4.功能实现分析... 5
4.1拓扑排序功能... 5
4.1.1具体实例... 5
4.1.2程序流程图... 6
4.1.3关键代码... 6
4.2关键路径功能... 7
4.2.1具体实例... 7
4.2.2程序流程图... 8
4.2.3关键代码... 8
4.3设计过程浅谈... 10
4.4源代码... 11
4.4.1拓扑排序... 11
4.4.2拓扑排序程序运行截图... 18
4.4.3关键路径... 18
4.4.4关键路径程序运行截图... 25
阅读了《数据结构(C语言)》的经典著作后,学习了有关简单算法的实现,认识到数学可
以应用到各个领域。本次算法课程设计运用所学的图论的拓扑排序和关键路径,去实现工程中的花费时间和顺利进行问题。拓扑排序主要用于检验工程能否施工,关键路径主要用于看出工程施工时间消耗。
本程序可以对给定输入的图进行拓扑排序以及利用拓扑排序的到的序列进行关键路径的求解,程序采用C语言代码实现,用到链表、图论等相关知识构成。
运行系统:Windows 10、Centos 7;
运行环境:C;
编译环境:CodeBlocks、Sublime Text 3;
编写数据结构广泛使用拓扑排序和关键路径算法。
使用代码实现拓扑排序,进而利用拓扑排序得到的序列球的关键路径。
用顶点表示活动,用弧表示活动间的优先关系的有向图为AOV网。
AOV网中没有环,检测的办法是进行拓扑排序。
步骤:
(1)在有向图中选一个没有前驱的顶点且输出之。
(2)从图中删除该顶点和所有以它为尾的弧。
重复上述两步,直至全部顶点均已输出,或者当前图中不存在无前驱的顶点为止。另一种情况则说明有向图中存在环。
与AOV网对应的是AOE网,即用边表示活动的网,AOE网是一个带权的有向无环图,顶点表示事件,弧表示活动,权表示活动持续的时间。通常用AOE网估计工程的完成时间。
由于在AOE网中有些活动可以并行的进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度。路径最长的叫关键路径,我们用e(i)表示活动最早开始时间,l(i)表示最迟开始时间。两者之差l(i)一e(i)意味着完成活动a的时间余量。我们把l(i)=e(i)的活动叫做关键活动。显然;关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程的进度。
1 |
4 |
6 |
2 |
3 |
5 |
3 |
4 |
2 |
5 |
2 |
5 |
5 |
1 |
4 |
6 |
1 |
2 |
4 |
3 |
5 |
实例所示为拓扑排序过程。
最终得到的拓扑序列:V1-V6-V4-V3-V2-V5
图的基本操作 |
4.1.2程序流程图
图的邻接存储 |
TopologicalSort(ALGraph G) |
得到拓扑序列 |
Status TopologicalSort(ALGraph G)
{ /* 有向图G采用邻接表存储结构。若G无回路,则输出G的顶点的一个拓扑序列并返回OK, */
/* 否则返回ERROR。算法7.12 */
int i,k,count,indegree[MAX_VERTEX_NUM];
SqStack S;
ArcNode *p;
FindInDegree(G,indegree); /* 对各顶点求入度indegree[0..vernum-1] */
InitStack(&S); /* 初始化栈 */
for(i=0;i if(!indegree[i]) Push(&S,i); /* 入度为0者进栈 */ count=0; /* 对输出顶点计数 */ while(!StackEmpty(S)) { /* 栈不空 */ Pop(&S,&i); printf("%s ",G.vertices[i].data); /* 输出i号顶点并计数 */ ++count; for(p=G.vertices[i].firstarc;p;p=p->nextarc) { /* 对i号顶点的每个邻接点的入度减1 */ k=p->adjvex; if(!(--indegree[k])) /* 若入度减为0,则入栈 */ Push(&S,k); } } if(count { printf("此有向图有回路\n"); return ERROR; } else { printf("为一个拓扑序列。\n"); return OK; } } 栈操作 图的邻接表 4.2.2程序流程图 图的基本操作 广度优先遍历 StatusTopologicalSort(ALGraph G) 求得关键路径 队列操作 拓扑排序 Status TopologicalOrder(ALGraph G,SqStack *T) { /* 算法7.13 有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve */ /* (全局变量)。T为拓扑序列顶点栈,S为零入度顶点栈。若G无回路,则用栈T */ /* 返回G的一个拓扑序列,且函数值为OK,否则为ERROR */ int j,k,count,indegree[MAX_VERTEX_NUM]; SqStack S; ArcNode *p; FindInDegree(G,indegree);/*对各顶点求入度indegree[0..vernum-1] */ InitStack(&S); /* 初始化栈 */ for(j=0;j if(!indegree[j]) Push(&S,j); /* 入度为0者进栈 */ InitStack(T); /* 初始化拓扑序列顶点栈 */ count=0; /* 对输出顶点计数 */ for(j=0;j ve[j]=0; while(!StackEmpty(S)) { /* 栈不空 */ Pop(&S,&j); Push(T,j); /* j号顶点入T栈并计数 */ ++count; for(p=G.vertices[j].firstarc;p;p=p->nextarc) { /* 对j号顶点的每个邻接点的入度减1 */ k=p->adjvex; if(--indegree[k]==0) /* 若入度减为0,则入栈 */ Push(&S,k); if(ve[j]+*(p->info)>ve[k]) ve[k]=ve[j]+*(p->info); } } if(count { printf("此有向网有回路\n"); return ERROR; } else return OK; } Status CriticalPath(ALGraph G) { /* 算法7.14 G为有向网,输出G的各项关键活动 */ int vl[MAX_VERTEX_NUM]; SqStack T; int i,j,k,ee,el; ArcNode *p; char dut,tag; if(!TopologicalOrder(G,&T)) /* 产生有向环 */ return ERROR; j=ve[0]; for(i=1;i if(ve[i]>j) j=ve[i]; for(i=0;i vl[i]=j; /* 完成点的最早发生时间 */ while(!StackEmpty(T)) /* 按拓扑逆序求各顶点的vl值 */ for(Pop(&T,&j),p=G.vertices[j].firstarc;p;p=p->nextarc) { k=p->adjvex; dut=*(p->info); /* dut if(vl[k]-dut vl[j]=vl[k]-dut; } printf(" j k dut ee el tag\n"); for(j=0;j for(p=G.vertices[j].firstarc;p;p=p->nextarc) { k=p->adjvex; dut=*(p->info); ee=ve[j]; el=vl[k]-dut; tag=(ee==el)?'*':' '; printf("%2d %2d %3d %3d %3d %c\n",j,k,dut,ee,el,tag); /* 输出关键活动 */ } printf("关键活动为:\n"); for(j=0;j for(p=G.vertices[j].firstarc;p;p=p->nextarc) { k=p->adjvex; dut=*(p->info); if(ve[j]==vl[k]-dut) printf("%s→%s\n",G.vertices[j].data,G.vertices[k].data); /* 输出关键活动 */ } return OK; } 在大一一学年的时间,初识了C语言和JAVA语言,学习了编程的基本功,认识到技术的道路漫长而遥远,只有不断的通过自学和向别人学习,不断保持进步的节奏。提高人生视野来认识更多的编程方法,去创新。 学习是需要付出时间和精力的,一直有输入才能厚积薄发,在学习数据结构课程时,一开始也是感觉很难,不知道无从下手,曾经纠结迷茫也失去信心。但随着时间的推进,好多困难渐渐克服,发现温故而知新的力量是真的强大。从一开始认识算法的时间复杂度和空间复杂度,进一步认识到线性结构和非线性结构,又从中学习了链表、栈、队列、串,到后来的树和图论。这些东西循序渐进,慢慢的组成了一张自己对于数据结构的知识网络。 “路漫漫其修远兮,吾将上下而求索”是教会我们学无止境,书山有路,勤能补拙大丈夫。 #include #include #include #include #include #include /* 函数结果状态代码 */ #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */ typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */ #define MAX_NAME 5 /* 顶点字符串的最大长度 */ typedef int InfoType; typedef char VertexType[MAX_NAME]; /* 字符串类型 */ /* 图的邻接表存储表示 */ #define MAX_VERTEX_NUM 20 /*网和图的区别是图没有权值,网有权值*/ typedef enum{DG,DN,AG,AN}GraphKind; /* 用枚举类型建立图的种类 {有向图,有向网,无向图,无向网} */ typedef struct ArcNode { int adjvex; /* 该弧所指向的顶点的位置 */ struct ArcNode *nextarc; /* 指向下一条弧的指针 */ InfoType *info; /* 网的权值指针) */ }ArcNode; /* 表结点 */ typedef struct { VertexType data; /* 顶点信息 */ ArcNode *firstarc; /* 第一个表结点的地址,指向第一条依附该顶点的弧的指针 */ }VNode,AdjList[MAX_VERTEX_NUM]; /* 头结点 */ typedef struct { AdjList vertices; int vexnum,arcnum; /* 图的当前顶点数和弧数 */ int kind; /* 图的种类标志 */ }ALGraph; /* 图的邻接表存储的基本操作(15个) */ int LocateVex(ALGraph G,VertexType u) { /* 初始条件: 图G存在,u和G中顶点有相同特征 */ /* 操作结果: 若G中存在顶点u,则返回该顶点在图中位置;否则返回-1 */ int i; for(i=0;i if(strcmp(u,G.vertices[i].data)==0) //比较顶点是否相等 return i; return -1; } Status CreateGraph(ALGraph *G) { /* 采用邻接表存储结构,构造没有相关信息的图G(用一个函数构造4种图) */ int i,j,k; int w; /* 权值 */ VertexType va,vb; ArcNode *p; printf("请输入图的类型(有向图:0,有向网:1,无向图:2,无向网:3): "); scanf("%d",&(*G).kind); printf("请输入图的顶点数,边数: "); scanf("%d,%d",&(*G).vexnum,&(*G).arcnum); printf("请输入%d个顶点的值(<%d个字符):\n",(*G).vexnum,MAX_NAME); for(i=0;i<(*G).vexnum;++i) /* 构造顶点向量 */ { scanf("%s",(*G).vertices[i].data); (*G).vertices[i].firstarc=NULL; } if((*G).kind==1||(*G).kind==3) /* 网 */ printf("请顺序输入每条弧(边)的权值、弧尾和弧头(以空格作为间隔):\n"); else /* 图 */ printf("请顺序输入每条弧(边)的弧尾和弧头(以空格作为间隔):\n"); for(k=0;k<(*G).arcnum;++k) /* 构造表结点链表 */ { if((*G).kind==1||(*G).kind==3) /* 网 */ scanf("%d%s%s",&w,va,vb); else /* 图 */ scanf("%s%s",va,vb); i=LocateVex(*G,va); /* 弧尾 */ j=LocateVex(*G,vb); /* 弧头 */ p=(ArcNode*)malloc(sizeof(ArcNode)); p->adjvex=j; if((*G).kind==1||(*G).kind==3) /* 网 */ { p->info=(int *)malloc(sizeof(int)); *(p->info)=w; //网的权值指针 } else p->info=NULL; /* 图 */ p->nextarc=(*G).vertices[i].firstarc; /* 插在表头 */ (*G).vertices[i].firstarc=p; if((*G).kind>=2) /* 无向图或网,产生第二个表结点 */ { p=(ArcNode*)malloc(sizeof(ArcNode)); p->adjvex=i; if((*G).kind==3) /* 无向网 */ { p->info=(int*)malloc(sizeof(int)); *(p->info)=w; } else p->info=NULL; /* 无向图 */ p->nextarc=(*G).vertices[j].firstarc; /* 插在表头 */ (*G).vertices[j].firstarc=p; } } return OK; } void Display(ALGraph G) { /* 输出图的邻接矩阵G */ int i; ArcNode *p; switch(G.kind) { case DG: printf("有向图\n"); break; case DN: printf("有向网\n"); break; case AG: printf("无向图\n"); break; case AN: printf("无向网\n"); } printf("%d个顶点:\n",G.vexnum); for(i=0;i printf("%s ",G.vertices[i].data); printf("\n%d条弧(边):\n",G.arcnum); for(i=0;i { p=G.vertices[i].firstarc; while(p) { if(G.kind<=1) /* 有向 */ { printf("%s→%s ",G.vertices[i].data,G.vertices[p->adjvex].data); if(G.kind==DN) /* 网 */ printf(":%d ",*(p->info)); } else /* 无向(避免输出两次) */ { if(i { printf("%s-%s ",G.vertices[i].data,G.vertices[p->adjvex].data); if(G.kind==AN) /* 网 */ printf(":%d ",*(p->info)); } } p=p->nextarc; } printf("\n"); } } void FindInDegree(ALGraph G,int indegree[]) { /* 求顶点的入度,算法7.12、7.13调用 */ int i; ArcNode *p; for(i=0;i indegree[i]=0; /* 赋初值 */ for(i=0;i { p=G.vertices[i].firstarc; while(p) { indegree[p->adjvex]++; p=p->nextarc; } } } typedef int SElemType; /* 栈类型 */ #define STACK_INIT_SIZE 10 /* 存储空间初始分配量 */ #define STACKINCREMENT 2 /* 存储空间分配增量 */ typedef struct SqStack { SElemType *base; /* 在栈构造之前和销毁之后,base的值为NULL */ SElemType *top; /* 栈顶指针 */ int stacksize; /* 当前已分配的存储空间,以元素为单位 */ }SqStack; /* 顺序栈 */ /* 顺序栈的基本操作(9个) */ Status InitStack(SqStack *S) { /* 构造一个空栈S */ (*S).base=(SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType)); if(!(*S).base) exit(OVERFLOW); /* 存储分配失败 */ (*S).top=(*S).base; (*S).stacksize=STACK_INIT_SIZE; return OK; } Status DestroyStack(SqStack *S) { /* 销毁栈S,S不再存在 */ free((*S).base); (*S).base=NULL; (*S).top=NULL; (*S).stacksize=0; return OK; } Status ClearStack(SqStack *S) { /* 把S置为空栈 */ (*S).top=(*S).base; return OK; } Status StackEmpty(SqStack S) { /* 若栈S为空栈,则返回TRUE,否则返回FALSE */ if(S.top==S.base) return TRUE; else return FALSE; } int StackLength(SqStack S) { /* 返回S的元素个数,即栈的长度 */ return S.top-S.base; } Status GetTop(SqStack S,SElemType *e) { /* 若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR */ if(S.top>S.base) { *e=*(S.top-1); return OK; } else return ERROR; } Status Push(SqStack *S,SElemType e) { /* 插入元素e为新的栈顶元素 */ if((*S).top-(*S).base>=(*S).stacksize) /* 栈满,追加存储空间 */ { (*S).base=(SElemType *)realloc((*S).base,((*S).stacksize+STACKINCREMENT)*sizeof(SElemType)); if(!(*S).base) exit(OVERFLOW); /* 存储分配失败 */ (*S).top=(*S).base+(*S).stacksize; (*S).stacksize+=STACKINCREMENT; } *((*S).top)++=e; return OK; } Status Pop(SqStack *S,SElemType *e) { /* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */ if((*S).top==(*S).base) return ERROR; *e=*--(*S).top; return OK; } Status StackTraverse(SqStack S,Status(*visit)(SElemType)) { /* 从栈底到栈顶依次对栈中每个元素调用函数visit()。 */ /* 一旦visit()失败,则操作失败 */ while(S.top>S.base) visit(*S.base++); printf("\n"); return OK; } Status TopologicalSort(ALGraph G) { /* 有向图G采用邻接表存储结构。若G无回路,则输出G的顶点的一个拓扑序列并返回OK, */ /* 否则返回ERROR。算法7.12 */ int i,k,count,indegree[MAX_VERTEX_NUM]; SqStack S; ArcNode *p; FindInDegree(G,indegree); /* 对各顶点求入度indegree[0..vernum-1] */ InitStack(&S); /* 初始化栈 */ for(i=0;i if(!indegree[i]) Push(&S,i); /* 入度为0者进栈 */ count=0; /* 对输出顶点计数 */ printf("("); while(!StackEmpty(S)) { /* 栈不空 */ Pop(&S,&i); printf("%s ",G.vertices[i].data); /* 输出i号顶点并计数 */ ++count; for(p=G.vertices[i].firstarc;p;p=p->nextarc) { /* 对i号顶点的每个邻接点的入度减1 */ k=p->adjvex; if(!(--indegree[k])) /* 若入度减为0,则入栈 */ Push(&S,k); } } printf(")"); if(count { printf("此有向图有回路\n"); return ERROR; } else { printf("为一个拓扑序列。\n"); return OK; } } void main() { ALGraph f; printf("请选择有向图\n"); CreateGraph(&f); Display(f); TopologicalSort(f); } void Display(ALGraph G) { /* 输出图的邻接矩阵G */ int i; ArcNode *p; switch(G.kind) { case DG: printf("有向图\n"); break; case DN: printf("有向网\n"); break; case AG: printf("无向图\n"); break; case AN: printf("无向网\n"); } printf("%d个顶点:\n",G.vexnum); for(i=0;i printf("%s ",G.vertices[i].data); printf("\n%d条弧(边):\n",G.arcnum); for(i=0;i { p=G.vertices[i].firstarc; while(p) { if(G.kind<=1) /* 有向 */ { printf("%s→%s ",G.vertices[i].data,G.vertices[p->adjvex].data); if(G.kind==DN) /* 网 */ printf(":%d ",*(p->info)); } else /* 无向(避免输出两次) */ { if(i { printf("%s-%s ",G.vertices[i].data,G.vertices[p->adjvex].data); if(G.kind==AN) /* 网 */ printf(":%d ",*(p->info)); } } p=p->nextarc; } printf("\n"); } } int ve[MAX_VERTEX_NUM]; /* 全局变量(用于算法7.13和算法7.14) */ void FindInDegree(ALGraph G,int indegree[]) { /* 求顶点的入度,算法7.12、7.13调用 */ int i; ArcNode *p; for(i=0;i indegree[i]=0; /* 赋初值 */ for(i=0;i { p=G.vertices[i].firstarc; while(p) { indegree[p->adjvex]++; p=p->nextarc; } } } typedef int SElemType; /* 栈类型 */ #define STACK_INIT_SIZE 10 /* 存储空间初始分配量 */ #define STACKINCREMENT 2 /* 存储空间分配增量 */ typedef struct SqStack { SElemType *base; /* 在栈构造之前和销毁之后,base的值为NULL */ SElemType *top; /* 栈顶指针 */ int stacksize; /* 当前已分配的存储空间,以元素为单位 */ }SqStack; /* 顺序栈 */ /* 顺序栈的基本操作(9个) */ Status InitStack(SqStack *S) { /* 构造一个空栈S */ (*S).base=(SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType)); if(!(*S).base) exit(OVERFLOW); /* 存储分配失败 */ (*S).top=(*S).base; (*S).stacksize=STACK_INIT_SIZE; return OK; } Status DestroyStack(SqStack *S) { /* 销毁栈S,S不再存在 */ free((*S).base); (*S).base=NULL; (*S).top=NULL; (*S).stacksize=0; return OK; } Status ClearStack(SqStack *S) { /* 把S置为空栈 */ (*S).top=(*S).base; return OK; } Status StackEmpty(SqStack S) { /* 若栈S为空栈,则返回TRUE,否则返回FALSE */ if(S.top==S.base) return TRUE; else return FALSE; } int StackLength(SqStack S) { /* 返回S的元素个数,即栈的长度 */ return S.top-S.base; } Status GetTop(SqStack S,SElemType *e) { /* 若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR */ if(S.top>S.base) { *e=*(S.top-1); return OK; } else return ERROR; } Status Push(SqStack *S,SElemType e) { /* 插入元素e为新的栈顶元素 */ if((*S).top-(*S).base>=(*S).stacksize) /* 栈满,追加存储空间 */ { (*S).base=(SElemType *)realloc((*S).base,((*S).stacksize+STACKINCREMENT)*sizeof(SElemType)); if(!(*S).base) exit(OVERFLOW); /* 存储分配失败 */ (*S).top=(*S).base+(*S).stacksize; (*S).stacksize+=STACKINCREMENT; } *((*S).top)++=e; return OK; } Status Pop(SqStack *S,SElemType *e) { /* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */ if((*S).top==(*S).base) return ERROR; *e=*--(*S).top; return OK; } Status StackTraverse(SqStack S,Status(*visit)(SElemType)) { /* 从栈底到栈顶依次对栈中每个元素调用函数visit()。 */ /* 一旦visit()失败,则操作失败 */ while(S.top>S.base) visit(*S.base++); printf("\n"); return OK; } Status TopologicalOrder(ALGraph G,SqStack *T) { /* 算法7.13 有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve */ /* (全局变量)。T为拓扑序列顶点栈,S为零入度顶点栈。若G无回路,则用栈T */ /* 返回G的一个拓扑序列,且函数值为OK,否则为ERROR */ int j,k,count,indegree[MAX_VERTEX_NUM]; SqStack S; ArcNode *p; FindInDegree(G,indegree);/*对各顶点求入度indegree[0..vernum-1] */ InitStack(&S); /* 初始化栈 */ for(j=0;j if(!indegree[j]) Push(&S,j); /* 入度为0者进栈 */ InitStack(T); /* 初始化拓扑序列顶点栈 */ count=0; /* 对输出顶点计数 */ for(j=0;j ve[j]=0; while(!StackEmpty(S)) { /* 栈不空 */ Pop(&S,&j); Push(T,j); /* j号顶点入T栈并计数 */ ++count; for(p=G.vertices[j].firstarc;p;p=p->nextarc) { /* 对j号顶点的每个邻接点的入度减1 */ k=p->adjvex; if(--indegree[k]==0) /* 若入度减为0,则入栈 */ Push(&S,k); if(ve[j]+*(p->info)>ve[k]) ve[k]=ve[j]+*(p->info); } } if(count { printf("此有向网有回路\n"); return ERROR; } else return OK; } Status CriticalPath(ALGraph G) { /* 算法7.14 G为有向网,输出G的各项关键活动 */ int vl[MAX_VERTEX_NUM]; SqStack T; int i,j,k,ee,el; ArcNode *p; char dut,tag; if(!TopologicalOrder(G,&T)) /* 产生有向环 */ return ERROR; j=ve[0]; for(i=1;i if(ve[i]>j) j=ve[i]; for(i=0;i vl[i]=j; /* 完成点的最早发生时间 */ while(!StackEmpty(T)) /* 按拓扑逆序求各顶点的vl值 */ for(Pop(&T,&j),p=G.vertices[j].firstarc;p;p=p->nextarc) { k=p->adjvex; dut=*(p->info); /* dut if(vl[k]-dut vl[j]=vl[k]-dut; } printf(" j k dut ee el tag\n"); for(j=0;j for(p=G.vertices[j].firstarc;p;p=p->nextarc) { k=p->adjvex; dut=*(p->info); ee=ve[j]; el=vl[k]-dut; tag=(ee==el)?'*':' '; printf("%2d %2d %3d %3d %3d %c\n",j,k,dut,ee,el,tag); /* 输出关键活动 */ } printf("关键活动为:\n"); for(j=0;j for(p=G.vertices[j].firstarc;p;p=p->nextarc) { k=p->adjvex; dut=*(p->info); if(ve[j]==vl[k]-dut) printf("%s→%s\n",G.vertices[j].data,G.vertices[k].data); /* 输出关键活动 */ } return OK; } void main() { ALGraph h; printf("请选择有向网\n"); CreateGraph(&h); Display(h); CriticalPath(h); }4.2关键路径功能
4.2.1具体实例
4.2.3关键代码
4.3设计过程浅谈
4.4源代码
4.4.1拓扑排序
4.4.2拓扑排序程序运行截图
4.4.3关键路径
4.4.4关键路径程序运行截图