文章目录
拓扑排序和关键路径
摘 要
介绍求关键路经的算法,对于给出的事件结点网络,要求求出从起点到终点的所有路径,经分析、比较后找出长读最大的路径,从而得出求关键路径的算法,并给出计算机上机实现的源程序。
关键词 关键路径 ( 最少时间 )
一、引言
通常把计划、施工过程、生产流程、程序流程的都当成一个工程。除了很小的工程外、一般都把工程分为若干个叫做“活动”的子工程。完成了这些“活动”的子工程,这个工程就可以完成了。
通常我们用有向图表示一个工程。在这种有向图中,用顶点表示活动,用有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行。如果在无有向环的带权有向图中用有向边表示一个工程中的各项活动(ACTIVITY),用有向边上的权值表示活动的持续时间(DURATION),用顶点表示事件(EVENT),则这种的有向图叫做用边表示活动的网络,简称AOE(active on edges)网络。
AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge Network)。
AOE网络在某些工程估算方面非常有用。他可以使人们了解:
(1):研究某个工程至少需要多少时间?
(2):那些活动是影响工程进度的关键?
在AOE网络中,有些活动可以并行的进行。从源点到各个顶点,以至从源点到汇点的有向路径可能不止一条。这些路径的长度也可能不同。完成不同路径的活动所需的时间虽然不同,但只有各条路径上所有活动都完成了,这个工程才算完成。因此,完成整个工程所需的时间取决于从源点到汇点的最长路径长度,即在这条路径上所有活动的持续时间之和。这条路径长度就叫做关键路径(critical path)。
二、设计步骤:
1: 以某一工程为蓝本,采用图的结构表示实际的工程计划的时间。
2: 调查以分析和预测这个工程计划个阶段的时间。
3: 用调查的结果建立AOE网(Activity On Edge Network),即边表示活动的网络,并用图的形式表示。
4: 用图来存储这些信息。
5: 用CreateGraphic();函数建立AOE图。
6: 用SearchMapPath();函数求出最大路径,并打印出关键路径。
7:编写代码
8: 测试
1、etv和ltv数组的计算方法
P[k]表示所有到达顶点Vk的弧的集合,即Vk是弧的终点。
S[k]表示所有从顶点Vk出发的弧的集合,即Vk是弧的起点
2、算法思想
关键路径算法由etv和ltv求得ete和lte,当ete与lte相等时表示活动没有空闲时间,是关键活动。
三. 实现代码
1、建立(以邻接表为例)
2、邻接表建立
#define MVNum 100
#define OK 1
#define ERROR 0
int topo[MVNum]; //定义拓扑排序数组
//邻接表结构的相关定义
typedef struct ArcNode { //边表
int adjvex;//该边所指向的顶点位置
struct ArcNode* nextarc;//指向下一条边的指针
int info; //和边相关信息,权值
}ArcNode;
typedef struct VNode { //表头结点表
char data;
ArcNode* firstarc;
}VNode, AdjList[MVNum];
typedef struct { //邻接表,带权有向图
AdjList vertices;
int vexnum, arcnum;
}ALGraph;
//函数返回顶点所在位置
int LocateVex(ALGraph G, char c) {
int i;
for (i = 0;i < G.vexnum;++i) {
if (c == G.vertices[i].data)
return i;
}
return -1;
}
//函数用邻接表创建有向无环图
int CreateDAG(ALGraph& G) {
int i, j, k, weight;
char v1, v2;
ArcNode* p;
printf("\n[请输入总顶点与总边数]:\n>>>");
scanf_s("%d %d", &G.vexnum, &G.arcnum);
for (i = 0;i < G.vexnum;i++) { //输入各点,构造表头结点表
printf("\n[请依次输入顶点信息]:\n>>>");
getchar();
scanf_s("%c", &G.vertices[i].data,1);
G.vertices[i].firstarc = NULL;
}
for (k = 0;k < G.arcnum;k++) {
printf("\n[请输入各边及权值构造邻接表]:\n>>>");
getchar();
scanf_s("%c %c %d", &v1,1, &v2,1, &weight);
i = LocateVex(G, v1);
j = LocateVex(G, v2);
p = (ArcNode*)malloc(sizeof(ArcNode));
p->adjvex = j;
p->info = weight;
p->nextarc = G.vertices[i].firstarc;
G.vertices[i].firstarc = p;
}
return OK;
}
3、栈的建立
//链栈的相关定义
typedef struct StackLink {
int data;
struct StackLink* next;
}StackLink, * StackNode;
//栈的初始化
int InitStack(StackNode& S) {
S = NULL;
return OK;
}
//入栈
int Push(StackNode& S, int e) {
StackNode p;
p = (StackLink*)malloc(sizeof(StackLink));
p->data = e;
p->next = S;
S = p;
return OK;
}
//出栈
int Pop(StackNode& S, int& e) {
StackNode p;
p = (StackLink*)malloc(sizeof(StackLink));
if (S == NULL)
return ERROR;
e = S->data; //将栈顶元素带出
p = S; //用P临时存放栈顶元素,已备释放
S = S->next;
free(p);
return OK;
}
//
int GetTop(StackNode S) {
if (S != NULL)
return S->data;
return -1;
}
//判断栈是否为空
int StackEmpty(StackNode S) {
if (S != NULL)
return ERROR;
else
return OK;
}
4、拓扑排序
//函数求出每个顶点入度存入数组 indegree[]
void FindInDegree(ALGraph G, int indegree[]) {
//求有向图邻接表顶点入度,两种方法:1、建立逆邻接表,2、遍历整个邻接表
//这里采用遍历整个邻接表
ArcNode* p;
int i;
for (i = 0;i < G.vexnum;i++) //入度初始化为零
indegree[i] = 0;
for (i = 0;i < G.vexnum;i++) { //遍历邻接表
p = G.vertices[i].firstarc;
while (p != NULL) {
indegree[p->adjvex]++;
p = p->nextarc;
}
}
}
//函数获得拓扑排序结果数组topo[]
int TopologicalSort(ALGraph G, int topo[]) {
//有向图G采用邻接表存储结构
//若G无回路,则生成G的一个拓扑序列topo[]并返回OK,否则返回ERROR
int i;
ArcNode* p;
StackNode S; //定义链栈
int indegree[MVNum];
FindInDegree(G, indegree); //求出各顶点入度,存入数组indegree中
printf("各顶点入度:");
printf("\n-----indegree-----\n");
for (i = 0;i < G.vexnum;i++) {
printf("%d", indegree[i]);
}
printf("\n-----indegree-----\n");
InitStack(S); //栈初始化为空
for (i = 0;i < G.vexnum;i++) {
if (indegree[i] == 0)
Push(S, i); //入度为零者入栈
}
int count = 0; //统计个数是,用于判断否全部打印
while (!StackEmpty(S)) {
Pop(S, i); //将栈顶顶点vi出栈
topo[count] = i; //将vi保存在拓扑序列数组topo中
count++; //对输出顶点计数
p = G.vertices[i].firstarc; //p指向第一个邻接点
while (p != NULL) {
int k = p->adjvex; //vk为vi的邻接点
indegree[k]--; //vi的每个邻接点入度减1
if (indegree[k] == 0)
Push(S, k); //若入度为0则入栈
p = p->nextarc; //p指向vi的下一个邻接点
}
}
if (count < G.vexnum) //判断有无回路
return ERROR;
else
return OK;
}
如果不了解拓扑排序可以观看拓扑排序。
5、关键路径的查找
int CriticalPath(ALGraph G) {
//G为邻接表存储的有向网,输出G的各项关键活动
ArcNode* p;
int etv[MVNum]; //etv[MVNum]记录每个事件最早发生时间
int ltv[MVNum]; //ltv[MVNum]记录每个事件最迟发生时间
int i, j, k, ete, lte;
if (!TopologicalSort(G, topo))
return ERROR; //调用拓扑排序算法,使拓扑序列保存在topo中;若调用失败,则存在有向环,返回ERROR
int n = G.vexnum; //n为顶点个数
for (i = 0;i < n;i++)
etv[i] = 0; //个每个事件的最早发生时间置初值0
/*-----------------按拓扑次序求每个事件最早发生时间-----------------*/
for (i = 0;i < n;i++) {
k = topo[i]; //取得拓扑排序序列中顶点序号k
p = G.vertices[k].firstarc; //p指向k的第一个邻接顶点
while (p != NULL) { //依次更新k的所有邻接顶点的最早发生时间
j = p->adjvex; //j为邻接顶点的序号
if (etv[j] < etv[k] + p->info) //更新顶点j的最早发生时间ve[j]
etv[j] = etv[k] + p->info;
p = p->nextarc; //p指向k的下一个邻接顶点
}
}
for (i = 0;i < n;i++) //给每个事件的最迟发生时间置初值ve[n-1]
ltv[i] = etv[n - 1];
/*-----------------按逆拓扑次序求每个事件最迟发生时间-----------------*/
for (i = n - 1;i >= 0;i--) {
k = topo[i]; //取得拓扑排序序列中顶点序号k
p = G.vertices[k].firstarc; //p指向k的第一个邻接顶点
while (p != NULL) { //根据k的邻接点,更新k的最迟发生时间
j = p->adjvex; //j为邻接顶点的序号
if (ltv[k] > ltv[j] - p->info) //更新顶点k的最早发生时间vl[k]
ltv[k] = ltv[j] - p->info; //应该取小的--->不然会延误工期,需要考虑其他路线
p = p->nextarc; //p指向k的下一个邻接顶点
}
}
/*-----------------判断每一活动是否为关键活动-----------------*/
printf("关键路径如下:\n\n");
for (i = 0;i < n;i++) {
p = G.vertices[i].firstarc; //p指向k的第一个邻接顶点
while (p != NULL) {
j = p->adjvex; //j为i的邻接顶点的序号
ete = etv[i]; //计算活动<vi,vj>的最早开始时间 相当于弧的最早发生时间
lte = ltv[j] - p->info; //计算活动<vi,vj>的最迟开始时间
if (ete == lte) //若为关键活动则输出<vi,vj>
printf("<%c,%c>", G.vertices[i].data, G.vertices[j].data);
p = p->nextarc; //p指向i的下一个邻接顶点
}
}
printf(" ->end\n");
return OK;
}
6、完整代码
//Authors:xiaobei
#include<stdio.h>
#include<stdlib.h>
#define MVNum 100
#define OK 1
#define ERROR 0
int topo[MVNum]; //定义拓扑排序数组
//邻接表结构的相关定义
typedef struct ArcNode { //边表
int adjvex;//该边所指向的顶点位置
struct ArcNode* nextarc;//指向下一条边的指针
int info; //和边相关信息,权值
}ArcNode;
typedef struct VNode { //表头结点表
char data;
ArcNode* firstarc;
}VNode, AdjList[MVNum];
typedef struct { //邻接表,带权有向图
AdjList vertices;
int vexnum, arcnum;
}ALGraph;
//链栈的相关定义
typedef struct StackLink {
int data;
struct StackLink* next;
}StackLink, * StackNode;
//栈的初始化
int InitStack(StackNode& S) {
S = NULL;
return OK;
}
//入栈
int Push(StackNode& S, int e) {
StackNode p;
p = (StackLink*)malloc(sizeof(StackLink));
p->data = e;
p->next = S;
S = p;
return OK;
}
//出栈
int Pop(StackNode& S, int& e) {
StackNode p;
p = (StackLink*)malloc(sizeof(StackLink));
if (S == NULL)
return ERROR;
e = S->data; //将栈顶元素带出
p = S; //用P临时存放栈顶元素,已备释放
S = S->next;
free(p);
return OK;
}
//
int GetTop(StackNode S) {
if (S != NULL)
return S->data;
return -1;
}
//判断栈是否为空
int StackEmpty(StackNode S) {
if (S != NULL)
return ERROR;
else
return OK;
}
//函数返回顶点所在位置
int LocateVex(ALGraph G, char c) {
int i;
for (i = 0;i < G.vexnum;++i) {
if (c == G.vertices[i].data)
return i;
}
return -1;
}
//函数用邻接表创建有向无环图
int CreateDAG(ALGraph& G) {
int i, j, k, weight;
char v1, v2;
ArcNode* p;
printf("\n[请输入总顶点与总边数]:\n>>>");
scanf_s("%d %d", &G.vexnum, &G.arcnum);
for (i = 0;i < G.vexnum;i++) { //输入各点,构造表头结点表
printf("\n[请依次输入顶点信息]:\n>>>");
getchar();
scanf_s("%c", &G.vertices[i].data,1);
G.vertices[i].firstarc = NULL;
}
for (k = 0;k < G.arcnum;k++) {
printf("\n[请输入各边及权值构造邻接表]:\n>>>");
getchar();
scanf_s("%c %c %d", &v1,1, &v2,1, &weight);
i = LocateVex(G, v1);
j = LocateVex(G, v2);
p = (ArcNode*)malloc(sizeof(ArcNode));
p->adjvex = j;
p->info = weight;
p->nextarc = G.vertices[i].firstarc;
G.vertices[i].firstarc = p;
}
return OK;
}
//函数求出每个顶点入度存入数组 indegree[]
void FindInDegree(ALGraph G, int indegree[]) {
//求有向图邻接表顶点入度,两种方法:1、建立逆邻接表,2、遍历整个邻接表
//这里采用遍历整个邻接表
ArcNode* p;
int i;
for (i = 0;i < G.vexnum;i++) //入度初始化为零
indegree[i] = 0;
for (i = 0;i < G.vexnum;i++) { //遍历邻接表
p = G.vertices[i].firstarc;
while (p != NULL) {
indegree[p->adjvex]++;
p = p->nextarc;
}
}
}
//函数获得拓扑排序结果数组topo[]
int TopologicalSort(ALGraph G, int topo[]) {
//有向图G采用邻接表存储结构
//若G无回路,则生成G的一个拓扑序列topo[]并返回OK,否则返回ERROR
int i;
ArcNode* p;
StackNode S; //定义链栈
int indegree[MVNum];
FindInDegree(G, indegree); //求出各顶点入度,存入数组indegree中
printf("各顶点入度:");
printf("\n-----indegree-----\n");
for (i = 0;i < G.vexnum;i++) {
printf("%d", indegree[i]);
}
printf("\n-----indegree-----\n");
InitStack(S); //栈初始化为空
for (i = 0;i < G.vexnum;i++) {
if (indegree[i] == 0)
Push(S, i); //入度为零者入栈
}
int count = 0; //统计个数是,用于判断否全部打印
while (!StackEmpty(S)) {
Pop(S, i); //将栈顶顶点vi出栈
topo[count] = i; //将vi保存在拓扑序列数组topo中
count++; //对输出顶点计数
p = G.vertices[i].firstarc; //p指向第一个邻接点
while (p != NULL) {
int k = p->adjvex; //vk为vi的邻接点
indegree[k]--; //vi的每个邻接点入度减1
if (indegree[k] == 0)
Push(S, k); //若入度为0则入栈
p = p->nextarc; //p指向vi的下一个邻接点
}
}
if (count < G.vexnum) //判断有无回路
return ERROR;
else
return OK;
}
int CriticalPath(ALGraph G) {
//G为邻接表存储的有向网,输出G的各项关键活动
ArcNode* p;
int etv[MVNum]; //etv[MVNum]记录每个事件最早发生时间
int ltv[MVNum]; //ltv[MVNum]记录每个事件最迟发生时间
int i, j, k, ete, lte;
if (!TopologicalSort(G, topo))
return ERROR; //调用拓扑排序算法,使拓扑序列保存在topo中;若调用失败,则存在有向环,返回ERROR
int n = G.vexnum; //n为顶点个数
for (i = 0;i < n;i++)
etv[i] = 0; //个每个事件的最早发生时间置初值0
/*-----------------按拓扑次序求每个事件最早发生时间-----------------*/
for (i = 0;i < n;i++) {
k = topo[i]; //取得拓扑排序序列中顶点序号k
p = G.vertices[k].firstarc; //p指向k的第一个邻接顶点
while (p != NULL) { //依次更新k的所有邻接顶点的最早发生时间
j = p->adjvex; //j为邻接顶点的序号
if (etv[j] < etv[k] + p->info) //更新顶点j的最早发生时间ve[j]
etv[j] = etv[k] + p->info;
p = p->nextarc; //p指向k的下一个邻接顶点
}
}
for (i = 0;i < n;i++) //给每个事件的最迟发生时间置初值ve[n-1]
ltv[i] = etv[n - 1];
/*-----------------按逆拓扑次序求每个事件最迟发生时间-----------------*/
for (i = n - 1;i >= 0;i--) {
k = topo[i]; //取得拓扑排序序列中顶点序号k
p = G.vertices[k].firstarc; //p指向k的第一个邻接顶点
while (p != NULL) { //根据k的邻接点,更新k的最迟发生时间
j = p->adjvex; //j为邻接顶点的序号
if (ltv[k] > ltv[j] - p->info) //更新顶点k的最早发生时间vl[k]
ltv[k] = ltv[j] - p->info; //应该取小的--->不然会延误工期,需要考虑其他路线
p = p->nextarc; //p指向k的下一个邻接顶点
}
}
/*-----------------判断每一活动是否为关键活动-----------------*/
printf("关键路径如下:\n\n");
for (i = 0;i < n;i++) {
p = G.vertices[i].firstarc; //p指向k的第一个邻接顶点
while (p != NULL) {
j = p->adjvex; //j为i的邻接顶点的序号
ete = etv[i]; //计算活动<vi,vj>的最早开始时间 相当于弧的最早发生时间
lte = ltv[j] - p->info; //计算活动<vi,vj>的最迟开始时间
if (ete == lte) //若为关键活动则输出<vi,vj>
printf("<%c,%c>", G.vertices[i].data, G.vertices[j].data);
p = p->nextarc; //p指向i的下一个邻接顶点
}
}
printf(" ->end\n");
return OK;
}
//菜单函数
void Menu() {
printf("\n---------菜单-------\n");
printf("\n1、创建图结构\n");
printf("\n2、拓扑排序\n");
printf("\n3、计算关键路径\n");
printf("\n0、退出\n");
printf("\n--------------------\n");
printf("\n[请输入你的选择:]\n>>>");
}
//主函数
int main() {
int i, user;
ALGraph G;
while (true) {
Menu();
scanf_s("%d", &user);
switch (user) {
case 1: {
if (CreateDAG(G))
printf("\n创建成功……\n");
break;
}
case 2: {
if (TopologicalSort(G, topo)) {
printf("拓扑排序结果如下:\n\n");
for (i = 0;i < G.vexnum;i++)
printf("%c->", G.vertices[topo[i]].data);
printf("end\n");
}
break;
}
case 3: {
CriticalPath(G);
break;
}
case 0:exit(0);
}
}
return 0;
}
7、实验结果
(1).创建邻接表
(2).拓扑排序
(3).关键路径
(4).退出
编译环境
Visual Studio 2019
四.后记
如果还有点不清楚,可以查看关键路径