拓扑排序和关键路径

拓扑排序和关键路径

摘 要

介绍求关键路经的算法,对于给出的事件结点网络,要求求出从起点到终点的所有路径,经分析、比较后找出长读最大的路径,从而得出求关键路径的算法,并给出计算机上机实现的源程序。
关键词 关键路径 ( 最少时间 )

一、引言

通常把计划、施工过程、生产流程、程序流程的都当成一个工程。除了很小的工程外、一般都把工程分为若干个叫做“活动”的子工程。完成了这些“活动”的子工程,这个工程就可以完成了。
通常我们用有向图表示一个工程。在这种有向图中,用顶点表示活动,用有向边<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

四.后记

如果还有点不清楚,可以查看关键路径

  • 21
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
拓扑排序是一种对有向无环图进行排序的算法,它可以得到一个有向无环图的线性序列。在拓扑排序中,每个顶点表示一个任务,每个有向边表示一个任务之间的依赖关系,即如果任务A依赖于任务B,则在序列中任务A必须在任务B之后。拓扑排序可以用来解决很多实际问题,如编译器的依赖关系分析、任务调度等。 关键路径是指在一个有向无环图中,从源点到汇点的所有路径中,耗时最长的路径称为关键路径关键路径上的任务称为关键任务,如果关键任务延迟了,整个项目的完成时间也会相应地延迟。因此,关键路径分析可以帮助我们找到项目中最关键的任务,以便优化项目进度。 拓扑排序的算法流程如下: 1. 统计每个顶点的入度(即有多少条边指向该顶点),并将入度为0的顶点加入队列。 2. 从队列中取出一个顶点,输出该顶点,并将该顶点的所有邻接点的入度减1。 3. 如果邻接点的入度为0,则将其加入队列。 4. 重复步骤2和3,直到队列为空。 关键路径的算法流程如下: 1. 对有向无环图进行拓扑排序,得到每个顶点的最早开始时间。 2. 从源点开始,按照拓扑序列依次计算每个顶点的最晚开始时间。 3. 对于每个任务,计算它的最早开始时间和最晚开始时间之差,即为该任务的总浮动时间。 4. 对于关键路径上的任务,它们的总浮动时间为0,因为它们不能延迟。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值