13--图的应用之关键路径

491 阅读9分钟

关于图的基础知识和图的存储相关实现,以及数据结构与算法的其他相关知识,请查看文章《数据结构与算法基础知识文章汇总》

在上一篇文章13--图的应用之拓扑排序已经介绍了图的拓扑排序的相关知识和实现方法,这篇文章将在图的拓扑排序的基础上分析一下图的关键路径的求法。

一、相关概念

AOE网

  • 1.在一个表示工程的带有权值的图中,用顶点表示事件,用有向边(弧)表示活动,用结弧上的权值表示活动的成本,这种有向图的边表表示活动的网,称为AOE网(Activity On Edge Network)。
  • 2.没有入边(没有被弧指向)的顶点称为始点或源点
  • 3.没有出边(没有弧去指向)的顶点称为终点或汇点
  • 4.路径上各个活动所持续的时间之和称为路径长度
  • 5.从源点汇点所经历的最大路径长度的路径称为关键路径
  • 6.在关键路径上的活动称为关键活动

由于工程只有一个开始和一个结束,所以AOE网只有一个源点和一个汇点。

二、图的关键路径的应用

image.png

上面造车的例子就是图的关键路径的最好应用,我们可以通过图的关键路径求出在制造汽车的各个工序中,哪个工序是最关键的,如果关键工序出现了延期,就会导致最终的交付时间延期。对于关键工序的优化,也是提升交付效率降低成本提升竞争力的重要因素。

理解关键路径: image.png

上图中,造外壳、造发动机、造轮子...等等的活动工序中,占用时间最长的是造发动机需要3天时间,也就是说发动机没造好前,无法进行“部件集中到位”这一步,即无法对汽车进行组装,所以造发动机就是这些活动工序中最重要的一个环节,它的能否顺利完成决定了整个汽车制造能否顺利完成,它的耗时也决定了整个汽车制造的最终耗时,所以它就是关键路径

三、图的关键路径

image.png

思考:有如上AOE网,我们何求得从V0~V9的关键路径?

1.选择合适的存储方式

拓扑排序的思考一样,这里选择邻接表来对图进行存储,原因就是方便顶点的删除操作

image.png

2.实现邻接表的存储

1.状态值和数据类型的定义

#define OK 1

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define MAXEDGE 30

#define MAXVEX 30

#define INFINITYC 65535

typedef int Status;    //Status是函数的类型,其值是函数结果状态代码,如OK等

2.数据结构的定义

//邻接矩阵数据结构
typedef struct
{
    int vexs[MAXVEX];

    int arc[MAXVEX][MAXVEX];

    int numVertexes, numEdges;

}MGraph;

//边表结点数据结构
typedef struct EdgeNode
{
    //邻接点域,存储该顶点对应的下标
    int adjvex;

    //用于存储权值,对于非网图可以不需要
    int weight;

    //链域,指向下一个邻接点
    struct EdgeNode *next;
}EdgeNode;

//顶点表结点的数据结构
typedef struct VertexNode
{
    //顶点入度
    int in;

    //顶点域,存储顶点信息
    int data;

    //边表头指针
    EdgeNode *firstedge;

}VertexNode, AdjList[MAXVEX];

//邻接表的结构
typedef struct
{
    AdjList adjList;

    //图中当前顶点数和边数
    int numVertexes,numEdges;
}graphAdjList,*GraphAdjList;

3.邻接矩阵的实现

//1.完成AOE网图关于邻接矩阵的存储
void CreateMGraph(MGraph *G)/* 构件图 */
{
    int i, j;

    /* printf("请输入边数和顶点数:"); */
    G->numEdges=13;
    G->numVertexes=10;
    
    for(i = 0; i < G->numVertexes; i++)//初始化图
    {
        G->vexs[i]=i;
    }

    for(i = 0; i < G->numVertexes; i++)//初始化图
    {
        for( j = 0; j < G->numVertexes; j++)
        {
            if (i==j)
                G->arc[i][j]=0;
            else
                G->arc[i][j]=INFINITYC;
        }
    }

    G->arc[0][1]=3;
    G->arc[0][2]=4;
    G->arc[1][3]=5;
    G->arc[1][4]=6;
    G->arc[2][3]=8;
    G->arc[2][5]=7;
    G->arc[3][4]=3;
    G->arc[4][6]=9;
    G->arc[4][7]=4;
    G->arc[5][7]=6;
    G->arc[6][9]=2;
    G->arc[7][8]=5;
    G->arc[8][9]=3;

}

4.邻接矩阵转换邻接表

void CreateALGraph(MGraph G,GraphAdjList *GL){

    int i,j;

    EdgeNode *e;

    *GL = (GraphAdjList)malloc(sizeof(graphAdjList));
    //赋值顶点数和边数
    (*GL)->numVertexes=G.numVertexes;
    (*GL)->numEdges=G.numEdges;
    
    //读入顶点信息,建立顶点表
    for(i= 0;i <G.numVertexes;i++)
    {
        (*GL)->adjList[i].in=0;
        (*GL)->adjList[i].data=G.vexs[i];

        //将边表置为空表
        (*GL)->adjList[i].firstedge = NULL;
    }

    //建立边表
    for(i=0;i<G.numVertexes;i++)
    {
        for(j=0;j<G.numVertexes;j++)
        {
            if(G.arc[i][j]!=0 && G.arc[i][j]<INFINITYC)
            {
                e=(EdgeNode *)malloc(sizeof(EdgeNode));

                //邻接序号为j
                e->adjvex=j;
                e->weight=G.arc[i][j];

                //将当前顶点上的指向的结点指针赋值给e
                e->next=(*GL)->adjList[i].firstedge;

                //将当前顶点的指针指向e
                (*GL)->adjList[i].firstedge=e;
                (*GL)->adjList[j].in++;
            }
        }
    }
}

5.图的拓扑排序求解,同求求得拓扑排序对应的顶点活动的最早发生时间

int *etv,*ltv; /* 事件最早发生时间和最迟发生时间数组,全局变量 */

int *stack2;   /* 用于存储拓扑序列的栈 */

int top2;       /* 用于stack2的指针*/

//拓扑排序
Status TopologicalSort(GraphAdjList GL){

    //若GL无回路,则输出拓扑排序序列且返回状态OK, 否则返回状态ERROR;
    EdgeNode *e;
    int i,k,gettop;

    //栈指针下标;
    int top = 0;

    //用于统计输出的顶点个数.作为拓扑排序是否存在回路的判断依据;
    int count = 0;

    //建栈,将入度in = 0的顶点入栈;
    int *stack = (int *)malloc(GL->numVertexes * sizeof(int));

    //遍历顶点表上入度in = 0 入栈
    for(i = 0; i < GL->numVertexes;i++) {
        //printf("%d %d\n",i,GL->adjList[i].in);
        if( 0 == GL->adjList[i].in ) {
            stack[++top] = i;
        }
    }

    //stack2 的栈指针下标
    top2 = 0;

    //初始化拓扑序列栈
    stack2 = (int *)malloc(sizeof(int) * GL->numVertexes);

    //* 事件最早发生时间数组
    etv = (int *)malloc(GL->numVertexes * sizeof(int));

    //初始化etv 数组
    for (i = 0 ; i < GL->numVertexes; i++) {
        //初始化
        etv[i] = 0;
    }

    printf("TopologicSort:\t");

    while(top != 0) {
        gettop = stack[top--];
        printf("%d -> ", GL->adjList[gettop].data);
        count++;

        //将弹出的顶点序号压入拓扑排序的栈中;
        stack2[++top2] = gettop;

        //例如gettop为V0 ,那么与V0相连接的结点就有etv[1] = 3; etv[2] = 4;
        //例如gettop为V1 ,那么与V1连接的结点就有etv[4]= 3+6=9; etv[3] = 8;
        //例如gettop为V2 ,那么与V2连接的结点就有etv[5]= 4+7=11; etv[3] = 12;
        //例如gettop为V3 ,那么与V3连接的结点就有etv[4]= 12+3=15;
        for(e = GL->adjList[gettop].firstedge; e; e = e->next)
        {
            k = e->adjvex;

            //将i顶点连接的邻接顶点入度减1,如果入度减一后为0,则入栈
            if(!(--GL->adjList[k].in))
                stack[++top] = k;

            //求各顶点事件的最早发生的时间etv值
            //printf("etv[gettop]+e->weight = %d\n",etv[gettop]+e->weight);
            //printf("etv[%d] = %d\n",k,etv[k]);
            if((etv[gettop] + e->weight) > etv[k]) {
                etv[k] = etv[gettop] + e->weight;
            }
        }
    }
    printf("\n");

    //打印etv(事件最早发生时间数组)
    for(i = 0; i < GL->numVertexes; i++) {
        printf("etv[%d] = %d\n",i,etv[i]);
    }
    printf("\n");
    
    if(count < GL->numVertexes)
        return ERROR;
    else
        return OK;
    return OK;
}

通过拓扑排序可以确定顶点活动发生的顺序,在拓扑排序的过程中,通过比较上一个顶点和与之相连的顶点之间的权值分别求和,如果最终得到的和大于原来存储的权值和,则更新stack2数组,stack2存储的就是拓扑排序对应顶点的最大权值和,即顶点活动的最早发生时间

image.png

V0->V1->V3的权值和为3+5 = 8,而V0->V2->V3的权值和为4+8 = 12,所以V3的最早发生时间为12

通过上面拓扑排序的过程求得事件最晚 发生时间etv和对应的顶点信息如下图: image.png

6.图的关键路径求解

void CriticalPath(GraphAdjList GL){

    EdgeNode *e;

    int i,gettop,k,j;

    //声明活动最早发生时间和最迟发生时间变量;
    int ete,lte;

    //求得拓扑序列,计算etv数组以及stack2的值
    TopologicalSort(GL);

    //打印etv数组(事件最早发生时间)
    printf("etv:\n");

    for(i = 0; i < GL->numVertexes; i++)
        printf("etv[%d] = %d \n",i,etv[i]);

    printf("\n");

    //事件最晚发生时间数组
    ltv = (int *)malloc(sizeof(int) * GL->numVertexes);

    //初始化ltv数组
    for(i = 0; i < GL->numVertexes; i++) {
        //初始化ltv数组. 赋值etv最后一个事件的值
        ltv[i] = etv[GL->numVertexes-1];
        
        //printf("ltv[%d] = %d\n",i,ltv[i]);
    }

    //计算ltv(事件最晚发生时间) 出栈求ltv
    while(top2 != 0) {
        //出栈(栈顶元素)
        gettop = stack2[top2--];
        
        //找到与栈顶元素连接的顶点; 例如V0是与V1和V2连接
        for(e = GL->adjList[gettop].firstedge; e; e = e->next) {
            //获取与gettop 相连接的顶点
            k = e->adjvex;
            
            //计算min(ltv[k]-e->weight,ltv[gettop])
            if(ltv[k] - e->weight < ltv[gettop]) {
                //更新ltv 数组
                ltv[gettop] = ltv[k] - e->weight;
            }
        }
    }

    //打印ltv 数组
    printf("ltv:\n");

    for(i = 0 ; i < GL->numVertexes; i++) {
        printf("ltv[%d] = %d \n",i,ltv[i]);
    }
    printf("\n");

    //求解ete,lte 并且判断lte与ete 是否相等.相等则是关键活动;

    //2层循环(遍历顶点表,边表)
    for(j=0; j<GL->numVertexes;j++)
    {
        for(e = GL->adjList[j].firstedge; e; e = e->next) {
            //获取与j连接的顶点;
            k = e->adjvex;

            //ete 就是表示活动 <Vk, Vj> 的最早开工时间, 是针对这条弧来说的.而这条弧的弧尾顶点Vk 的事件发生了, 它才可以发生. 因此ete = etv[k];
            ete = etv[j];

            //lte 表示活动<Vk, Vj> 的最晚开工时间, 但此活动再晚也不能等Vj 事件发生才开始,而是必须在Vj 事件之前发生. 所以lte = ltv[j] - len<Vk, Vj>.
            lte = ltv[k] - e->weight;

            //如果ete == lte 则输出j,k以及权值;
            if(ete == lte) {
                printf("<%d-%d> length:%d\n",GL->adjList[j].data, GL->adjList[k].data, e->weight);
            }
        }
    }
}
  • 1.ltv表示顶点活动的最晚发生时间,初始化它时,设置为汇点的最早发生时间,这个值最大,后面会更新;
  • 2.根据拓扑排序的顺序,从栈顶遍历stack栈中的所有顶点活动,找到与这个顶点有连接的顶点,如果与栈顶顶点的最早发生时间减去相连顶点的权值小于栈顶顶点原来记录的最早发生时间,则更新ltv数组;如下图所示,V8的最早发生时间为27,则27-3 = 24,24比27小,所以更新ltv[8] = 24;

image.png

  • 3.最终得到的etv数组(最晚发生数组)和ltv数组(最早发生数组)的数据如下:

image.png

  • 4.如果顶点活动的最晚发生时间ete和最早发生时间lte相同,说明这个顶点活动就是关键活动

image.png

7.调试代码

int main(int argc,const char * argv[]) {

    // insert code here...

    printf("Hello, 关键路径的求解!\n");

    MGraph G;

    GraphAdjList GL;

    CreateMGraph(&G);

    CreateALGraph(G,&GL);

    //拓扑排序
    //TopologicalSort(GL);
    
    //关键路径
    CriticalPath(GL);

    return 0;
}

8.调试输出

image.png

四、总结

  • 1.AOE网是带权值的特殊的AOV网,AOE网只有一个源点和一个汇点;
  • 2.图的关键路径的求解需要依赖于图的拓扑排序的构造过程;
  • 3.图的关键路径的求解过程很难理解