拓扑排序与关键路径

  1. 拓扑排序

一、定义

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网( Activity On VertexNetwork)。

若用DAG图(有向无环图)表示一个工程,其顶点表示活动,用有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行的这样一种关系。在AOV网中,活动Vi是活动Vj的直接前驱,活动Vj是活动Vi的直接后继,这种前驱和后继关系具有传递性,且任何活动Vi不能以它自己作为自己的前驱或后继。

设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2,...Vn,满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必在顶点Vj之前。则我们称这样的顶点序列为一个拓扑序列。

所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。每个AOV网都有一个或多个拓扑排序序列。

算法

对一个AOV网进行拓扑排序的算法有很多,下面介绍比较常用的一种方法的步骤:

①从AOV网中选择一个没有前驱的顶点并输出。

②从网中删除该顶点和所有以它为起点的有向边。

③重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。如果输出顶点数少了,哪怕是少了一个,也说明这个网存在环(回路),不是AOV网。

上图所示为拓扑排序过程的示例。每一轮选择一个入度为0的顶点并输出,然后删除该顶点和所有以它为起点的有向边,最后得到拓扑排序的结果为{1,2,4,3,5}。

算法实现

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAXVEX 20
typedef char VerType;    //顶点值类型

struct EdgeNode{
    int adjvex;    //邻接点域,存储该顶点对应的下标
    int weight;    //用于存储权值,对于非网图可以不需要
    EdgeNode* next;    //下一个结点 
};

struct VertexNode{
    int in;    //入度
    VerType data;    //值
    EdgeNode* firstedge;    //邻接表头指针 
}; 

struct Graph{
    VertexNode vers[MAXVEX];
    int numVertexes, numEdges;    //顶点数和边数 
};

//拓扑排序,若G没有回路,则输出拓扑排序序列并返回OK,若有回路返回ERROR 
bool TopologicalSort(Graph* G){
    EdgeNode* e;
    int i, k, gettop;
    int top = 0;    //栈指针下标
    int count = 0;    //统计输出顶点个数
    int* stack;    //存储入度为0的顶点
    stack = (int*)malloc(G->numVertexes * sizeof(int));
    
    for(i = 0;i<G->numVertexes;i++)    //遍历所有结点 
        if(G->vers[i].in == 0)
            stack[++top] = i;    //将入度为0的顶点入栈

    while(top != 0){
        gettop = stack[top--];    //出栈
        printf("%c ",G->vers[gettop].data);
        count++;    //统计输出顶点数
        for(e=G->vers[gettop].firstedge; e; e = e->next){
            //弧表遍历
            k = e->adjvex;
            if(!(--G->vers[k].in))    //将k号顶点邻接点的入度减1
                stack[++top] = k;    //若为0则入栈,以便下次循环输出 
        }
    }
    if(count < G->numVertexes)    //如果count小于顶点数,说明存在环
        return false;
    else
        return true; 
}

/* 图初始化 */
void CreateGraph(Graph* G){
    int i, m, n;
    
    printf("输入顶点数和边数:\n");
    scanf("%d %d",&G->numVertexes, &G->numEdges);
    printf("输入顶点值:\n");
    for(i=0;i<G->numVertexes;i++){
        getchar();    //吃掉回车
        scanf("%c",&G->vers[i].data);
    }
    //初始化图头结点指针和入度值 
    for(i=0;i<G->numVertexes;i++){
        G->vers[i].firstedge = NULL;
        G->vers[i].in = 0;    //入度为0 
    }
    printf("输入边:\n");
    for(i=0;i<G->numEdges;i++){
        scanf("%d %d",&m, &n);
        EdgeNode* newNode = (EdgeNode*)malloc(sizeof(EdgeNode));
        newNode->next = G->vers[m].firstedge == NULL ? NULL : G->vers[m].firstedge;
        newNode->adjvex = n;
        G->vers[m].firstedge = newNode;
        G->vers[n].in++;    //入度+1 
    }
}

int main(){
    
    Graph* G = (Graph*)malloc(sizeof(Graph));
    CreateGraph(G);
    if(TopologicalSort(G)){
        printf("拓扑排序完成!\n");
    }else{
        printf("图存在环");
    }
    return 0;
}
/*
6 8
a
b
c
d
e
f
0 1
0 2
0 4
1 3
3 2
2 4
4 5
3 5
结果 a b d c e f 拓扑排序完成!
12 16
a
b
c
d
e 
f 
g 
h 
i 
j
k  
l 
0 3
3 4
0 1
1 2
2 4
2 6
4 6
0 2
0 11
2 7
8 11
8 9
9 11
8 10
10 5
5 7
i j k f a d b c e g h l   
*/

2.关键路径

一、定义

拓扑排序主要是为解决一个工程能否顺序进行的问题,但有时我们还需要解决工程完成需要的最短时间问题。

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网。AOE网和AOV网都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,AOE网中的边有权值;而AOV网中的边无权值,仅表示顶点之间的前后关系。

AOE网具有以下两个性质:

①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;

②只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。

如上图的AOE网,在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;网中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动

完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。这是因为关键活动影响了整个工程的时间,即若关键活动不能按时完成,则整个工程的完成时间就会延长。因此,只要找到了关键活动,就找到了关键路径,也就可以得出最短完成时间。

算法

在分析算法之前,需要了解几个重要的参数:

1.事件的最早发生时间ve:即顶点Vk的最早发生时期。

2.事件的最晚发生时间vl:即顶点Vk的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超出此时间将会延误整个工期。

3.活动的最早开始时间e:即弧ai的最早发生时间。

4.活动的最晚开始时间l:即弧ai的最晚发生时间,也就是不推迟工期的最晚开工时间。

5.一个活动ai的最迟开始时间l(i)和其最早开始时间e(i)的差额d(i)=l(i)−e(i):它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动ai可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称l(i)−e(i)=0即l(i)=e(i)的活动ai是关键活动。

求关键路径的算法步骤如下:

1.从源点出发,令ve(源点)=0, 按拓扑排序求其余顶点的最早发生时间ve()。

2.从汇点出发,令vl(汇点)=ve(汇点),按逆拓扑排序求其余顶点的最迟发生时间vl()。

3.根据各顶点的ve()值求所有弧的最早开始时间e()。

4.根据各顶点的vl()值求所有弧的最迟开始时间l()。

5.求AOE网中所有活动的差额d(), 找出所有d()=0的活动构成关键路径

对于关键路径,需要注意以下几点:

①关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动就可能会变成非关键活动。

②网中的关键路径并不唯一,

且对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。

关键路径的实现

估算工程完成的最短时间,既是找从源点到汇点的带权路径长度最长的路径,称为关键路径

#include <cstdio>
#include <cstring>
#define MAXN 100 //The Max num of Vertex
#define MAXM 200 //The Max num of Edges
using namespace std;
 
struct ArcNode //保存边的信息
{
    int to, dur, no;
    //to: next vertex, dur: the duration of the activities; no: the ID of activity
    struct ArcNode *next;
};
 
//全局变量!
int n,m; //the number of Vertex and Edge,
ArcNode* outEdge[MAXN]; //记录每个顶点对应的出边表
ArcNode* inEdge[MAXN]; //记录每个顶点对应的入边表
int outOrd[MAXN]; //每个顶点的出度
int inOrd[MAXN]; //每个顶点的入度
int ev[MAXN]; //Earliest start time for Vertex
int lv[MAXN]; //Latest start time for Vertex
int ee[MAXM]; //MAXM!! Earliest start time for Edge
int le[MAXM]; //Latest start time for Edge!!
 
void CriticalPath()
{
    int i; //循环变量
    int tmp,nxt; //临时变量
    int top=-1; //top指示栈顶的位置;-1表示栈空,正整数表示下一个入度(或出度)为零的点的位置
    ArcNode* tpNode;
    for(i=0;i<n;i++) //扫描inOrd;把所有入度为0的点入栈(一个虚拟的栈,以top表示下一个数据的位置)
    {
        if(inOrd[i]==0)
        {
            inOrd[i]=top; //因为inOrd为0,失去了意义,所以正好可以以此来保存栈中下一个元素的位置
            top=i; //以这种类似于堆栈的方式,保存所有入度为0的点
        }
    }
 
    //可以明确的是,如果不存在环的话,必然每个顶点都会遍历一次,所以这里可以做一个循环
    //如果循环结束前,入度为0的点就用尽的话,必然是有环的
    for(i=0;i<n;i++)
    {
        if(-1==top)
        {
            printf("Cycle Detected!!\n");
            return;
        }
        else
        {
            tmp=top; //tmp记录当前需要处理的顶点号,即入度为0的点
            top=inOrd[top];//top中保存下一个入度为0的元素位置
            tpNode=outEdge[tmp];//取出入度为零点的出边链表
            while(tpNode!=NULL)
            {
                nxt=tpNode->to;
                inOrd[nxt]--; //从该点出发的所有终点的入度减1
                if(0==inOrd[nxt]) //若出现新的入度为零的点,则入栈
                {
                    inOrd[nxt]=top;
                    top=nxt;
                }
 
                //其它的都是套路(实现拓扑排序的套路),下面这两句才是为求关键路径而生的
                //下一个点的最早开始时间,必然是上一个点的最早开始时间+活动持续时间
                //如果到达该点有多个路径,最早开始时间必然是个值中的最大值!(因为有一条路径未完成,该点就不能启动)
                //第一个起点的ev值,在初始化时就被设为0了
                if(ev[nxt]<tpNode->dur+ev[tmp])
                    ev[nxt]=tpNode->dur+ev[tmp];
 
                tpNode=tpNode->next;
            }
        }
    }
 
 
    //以入度邻接表,再来一遍
    int maxtime=0;
    for(i=0;i<n;i++)  //找出工程所需时间(总时间)
        if(ev[i]>maxtime)
            maxtime=ev[i];
 
    top=-1; //重新设栈顶
    for(i=0; i<n; i++)
    {
        lv[i]=maxtime; //先将所有节点的最迟开始时间都设为最后时间
        if(0==outOrd[i]) //依然是设栈,解释见上面雷同程序
        {
            outOrd[i]=top;
            top=i;
        }
    }
    for(i=0; i<n; i++)
    {
        if(-1==top)
        {
            printf("Back Cycle Detected.\n");
            return;
        }
        else
        {
            tmp=top; 
            top=outOrd[top];
            tpNode=inEdge[tmp];
            while(tpNode!=NULL)
            {
                nxt=tpNode->to; //其实是找上一个点
                outOrd[nxt]--;
                if(0==outOrd[nxt])
                {
                    outOrd[nxt]=top;
                    top=nxt;
                }
                //下面两句计算最迟开始时间
                //只要有一条路径决定它在更早的时间开始,就得更早开始,所以取各路径最小值
                if(lv[nxt]>(lv[tmp]-tpNode->dur))
                    lv[nxt]=(lv[tmp]-tpNode->dur);
 
                tpNode=tpNode->next;
            }
        }
    }
 
    //上面计算的都是节点(!)的最早和最迟开始时间,下面需要计算边的
    //若边(活动)的最早开始==最迟开始时间,则该边为关键路径
    printf("The Critical Path:\n");
    for(i=0; i<n; i++)  //通过出边表,遍历每条边!!(但必须从顶点入手,理出每个顶点的出边表)
    {
        tpNode=outEdge[i];
        while(tpNode!=NULL)
        {
            tmp=tpNode->no; //tmp此时保存边的编号!!
            nxt=tpNode->to;
            ee[tmp]=ev[i];//边的最早开始时间就是其起点的最早开始时间
            le[tmp]=lv[nxt]-tpNode->dur; //边的最迟开始时间,是其终点的最迟开始时间减去边的持续时间
            if(ee[tmp]==le[tmp])
                printf("a%d:%d->%d\n",tmp,i,nxt);
            tpNode=tpNode->next;
        }
    }
}
 
 
int main()
{
    int i;
    int s,e,t; //start point; end point; time needed
    ArcNode* newNode; //只定义,未初始化
 
    memset(outEdge, NULL, sizeof(outEdge));
    memset(inEdge, NULL, sizeof(inEdge));
    memset(outOrd, 0, sizeof(outOrd)); //必须初始化为0
    memset(inOrd,0, sizeof(inOrd));
    memset(ev,0,sizeof(ev));
    memset(lv,0,sizeof(lv));
    memset(ee,0,sizeof(ee));
    memset(le,0,sizeof(le));
    printf ("输入顶点数和边数\n");
    scanf("%d %d",&n,&m); //读入输入数据,共计n个顶点和m条边
    printf("输入依附的顶点及权值\n");
    for(i=0;i<m;i++)
    {
        scanf("%d%d%d",&s,&e,&t);
 
        //构建出边表
        outOrd[s]++; //起点的出度增加
        newNode=new ArcNode; //初始化赋值
        newNode->to=e;
        newNode->no=i+1; //这个是边的编号,第一条读入的边作为1号边
        newNode->dur=t;
        newNode->next=NULL; //NULL需要大写!
        if(outEdge[s]==NULL)  //没有之前的出边,则直接赋值;若有,则需像挂接火车车厢一样,挂接链表
            outEdge[s]=newNode;
        else
        {
            newNode->next=outEdge[s];
            outEdge[s]=newNode;
        }
 
        //构建入边表
        inOrd[e]++;
        newNode=new ArcNode; // 必须重新赋值
        newNode->to=s;
        newNode->no=i+1;
        newNode->dur=t;
        newNode->next=NULL;
        if(inEdge[e]==NULL)
            inEdge[e]=newNode;
        else
        {
            newNode->next=inEdge[e];
            inEdge[e]=newNode;
        }
    }
 
    //一次性获得全部输入后,执行程序的核心部分——找出关键路径
    CriticalPath();
 
    //Release the Memory
    for(i=0;i<n;i++)
    {
        while(outEdge[i]!=NULL)
        {
            newNode=outEdge[i]->next;  //newNode不是新节点,只是借用一下其名字
            delete outEdge[i];
            outEdge[i]=newNode;
        }
 
        while(inEdge[i]!=NULL)
        {
            newNode=inEdge[i]->next;
            delete inEdge[i];
            inEdge[i]=newNode;
        }
    }
    return 0;
}
/*
9 11
0 1 6
1 4 1
4 6 9
6 8 2
0 2 4
2 4 1
4 7 7
7 8 4
0 3 5
3 5 2
5 7 4
*/

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
拓扑排序是一种对有向无环图进行排序的算法,它将图中的节点按照它们的依赖关系进行排序,使得每个节点在排列中都在它的后继节点之前。拓扑排序可以用于解决很多问题,例如任务调度、编译顺序等。 关键路径算法是一种用于确定项目中关键任务的算法。它通过计算每个任务的最早开始时间和最晚开始时间,来确定哪些任务对项目的完成时间具有关键作用。关键路径算法可以帮助项目管理者确定项目的关键路径,从而更好地控制项目的进度。 以下是拓扑排序关键路径算法的详细步骤: 1. 拓扑排序: - 创建一个空列表result和一个空集合visited。 - 遍历图中的每个节点,对于每个未访问过的节点,调用dfs函数进行深度优先遍历。 - 在dfs函数中,对于当前节点,将其加入visited集合中,并遍历其所有的后继节点。 - 对于每个后继节点,如果它未被访问过,则递归调用dfs函数。 - 在递归返回时,将当前节点加入result列表中。 - 最后将result列表反转,即可得到拓扑排序的结果。 2. 关键路径算法: - 对于每个任务,计算它的最早开始时间EST和最晚开始时间LST。 - 对于每个任务,计算它的最早完成时间EFT和最晚完成时间LFT。 - 对于每个任务,计算它的总浮动时间TF和自由浮动时间FF。 - 对于每个任务,如果它的TF为0,则它是关键任务。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值