从零开始养成算法·篇十八:图的应用之拓扑排序与关键路径

一、拓扑排序

有向无环图(DAG):

如果一个有向图不存在环,也就是任意结点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图。

AOV网络:

在有向图中,用顶点表示活动,用有向边 表示活动 i是活动 j 的必须条件。这种有向图称为用顶点表示活动的网络(Active on vertices),简称AOV网络。
在AOV网络中,如果活动Vi必须在Vj之前进行,则存在有向边 ,并称Vi是Vj的直接前驱,Vj是Vi的直接后继。这种前驱与后继的关系具有传递性和反自反性,这要求AOV网络中不能出现回路,即有向环。因此,对于给定的AOV网络,必须先判断它是否存在有向环。

拓扑排序:

检测有向环可以通过对AOV网络进行拓扑排序,该过程将各个顶点排列成一个线性有序的序列,使得AOV网络中所有的前驱和后继关系都能得到满足。 如果拓扑排序能够将AOV网络的所有顶点都排入一个拓扑有序的序列中,则说明该AOV网络中没有有向环,否则AOV网络中必然存在有向环。AOV网络的顶点的拓扑有序序列不唯一。可以将拓扑排序看做是将图中的所有节点在一条水平线上的展开,图的所有边都从左指向右。
用计算机专业的几门课程的学习次序来描述拓扑关系(打个比方,图内容可能不是特别严谨),显然对于一门课来说,必须先学习它的先导课程才能更好地学习这门课程,比如学数据结构必须先学习C语言和离散数学,而且先导课程中不能有环,否则没有尽头了(多玛姆,我是来谈条件的?)

1.png

而且还可以发现,如果两门课程之间没有直接或间接的先导关系,那么这两门课的学习先后是任意的(比如“C语言”和“离散数学”的学习顺序就是任意的),于是上述课程就可以排成一个水平展开的先后顺序,如下图

2.png

拓扑排序的结果不唯一,比如“C语言”和“离散数学”就可以换下顺序,又或者把“计算机导论”向前放在任何一个位置都可以。总结一下就是,如果某一门课没有先导课程或是所有的先导课程都已经学习完毕,那么这门课就可以学习了。如果同时有多门这样的课,它们的学习顺序任意。

算法描述:

对于一个有向无环图
(1)统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向所有的节点的入度-1。
(2)重复(1),直到所有的节点都被分离出来,拓扑排序结束。
(3)如果最后不存在入度为0的节点,那就说明有环,无解。

解释一下,假设A为一个入度为0的结点,就表示A结点没有前驱结点,可以直接做,把A完成后,对于A的所有后继结点来说,前驱结点就完成了一个,入度进行-1。

3.png

时间复杂度:

如果AOV网络有n个顶点,e条边,在拓扑排序的过程中,搜索入度为零的顶点所需的时间是O(n)。在正常情况下,每个顶点进一次栈,出一次栈,所需时间O(n)。每个顶点入度减1的运算共执行了e次。所以总的时间复杂为O(n+e)。

因为拓扑排序的结果不唯一,所以题目一般会要求按某种顺序输出,就需要使用优先级队列。

代码实现

/*邻接矩阵结构 */
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;


/*1.构成AOV网图*/
void CreateMGraph(MGraph *G)/* 构件图 */
{
   int i, j;
   
   /* printf("请输入边数和顶点数:"); */
   G->numEdges=MAXEDGE;
   G->numVertexes=MAXVEX;
   
   /* 初始化图 */
   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++)
       {
           G->arc[i][j]=0;
       }
   }
   
   G->arc[0][4]=1;
   G->arc[0][5]=1;
   G->arc[0][11]=1;
   G->arc[1][2]=1;
   G->arc[1][4]=1;
   G->arc[1][8]=1;
   G->arc[2][5]=1;
   G->arc[2][6]=1;
   G->arc[2][9]=1;
   G->arc[3][2]=1;
   G->arc[3][13]=1;
   G->arc[4][7]=1;
   G->arc[5][8]=1;
   G->arc[5][12]=1;
   G->arc[6][5]=1;
   G->arc[8][7]=1;
   G->arc[9][10]=1;
   G->arc[9][11]=1;
   G->arc[10][13]=1;
   G->arc[12][9]=1;
   
}

/*2.将AOV网图借助邻近矩阵转换成邻接表结构*/
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 adjList[i].in=0;
       (*GL)->adjList[i].data=G.vexs[i];
       //将边表置为空表
       (*GL)->adjList[i].firstedge=NULL;
   }
   
   //建立边表
   for(i=0;iadjvex=j;
               // 将当前顶点上的指向的结点指针赋值给e
               e->next=(*GL)->adjList[i].firstedge;
               //将当前顶点的指针指向e
               (*GL)->adjList[i].firstedge=e;
               (*GL)->adjList[j].in++;
               
           }
       }
   }
}

Status TopologicalSort(GraphAdjList GL){
   
   EdgeNode *e;
   int i,k,gettop;
   //用于栈指针下标
   int top=0;
   //用于统计输出顶点的个数
   int count=0;
   
   //建栈将入度为0的顶点入栈(目的:为了避免每次查找时都要遍历顶点表查找有没有入度为0的顶点)
   int *stack=(int *)malloc(GL->numVertexes * sizeof(int) );
   
   //1.遍历邻接表-顶点表,将入度in为0的顶点入栈
   /*参考图1> 此时stack栈中应该成为0,1,3.即V0,V1,V3的顶点入度为0*/
   for(i = 0; inumVertexes; I++)
       //将入度为0的顶点入栈
       if(0 == GL->adjList[i].in)
           stack[++top]=I;
   printf("top = %d\n",top);
   
   //2.循环栈结构(当栈中有元素则循环继续)
   while(top!=0)
   {
       //出栈
       gettop=stack[top--];
       printf("%d -> ",GL->adjList[gettop].data);
       
       //输出顶点,并计数
       count++;
       
       //遍历与栈顶相连接的弧
       for(e = GL->adjList[gettop].firstedge; e; e = e->next)
       {
           //获取与gettop连接的顶点
           k=e->adjvex;
           
           //1.将与gettop连接的顶点入度减1;
           //2.判断如果当前减1后为0,则入栈
           if( !(--GL->adjList[k].in) )
               //将k入栈到stack中,并且top加1;
               stack[++top]=k;
       }
   }
   printf("\n");
   
   //判断是否把所有的顶点都输出. 则表示找到了拓扑排序;
   if(count < GL->numVertexes)
       return ERROR;
   else
       return OK;
}

二、关键路径

AOE网示例图:


4.png

AOE网:在一个表示工程的带权有向图中,用顶点表示事件(如V0),用有向边表示活动(如 = a1),边上的权值表示活动的持续时间,称这样的有向图为边表示的活动的网,简称AOE网(activity on edge network)
源点:在AOE网中,没有入边的顶点称为源点;如顶点V0
终点:在AOE网中,没有出边的顶点称为终点;如顶点V3

AOE网的性质:
【1】只有在进入某顶点的活动都已经结束,该顶点所代表的事件才发生;
例如:a1和a2活动都结束了,顶点V2所代表的事件才会发生。
【2】只有在某顶点所代表的事件发生后,从该顶点出发的各活动才开始;
例如:只有顶点V1所代表的事件结束之后,活动a2和a4才会开始。

在AOE网中,所有活动都完成才能到达终点,因此完成整个工程所必须花费的时间(即最短工期)应该为源点到终点的最大路径长度。具有最大路径长度的路径称为关键路径。关键路径上的活动称为关键活动:

事件的最早发生时间:ve[k]
根据AOE网的性质,只有进入Vk的所有活动都结束,Vk代表的事件才能发生,而活动的最早结束时间为ve[j]+len。所以,计算Vk的最早发生时间的方法为:
ve[0] = 0
ve[k] = max(ve[j] + len)

事件的最迟发生时间:vl[k]
vl[k]是指在不推迟整个工期的前提下,事件Vk允许的最迟发生时间。根据AOE网的性质,只有顶点Vk代表的事件发生,从Vk出发的活动才能开始,而活动的最晚开始时间为vl[j] - len

活动的最早发生时间:ee[i]
ai由有向边,根据AOE网的性质,只有顶点Vk代表的事件发生,活动ai才能开始,即活动ai的最早开始时间等于事件Vk的最早开始时间。

活动的最迟发生时间:el[i]
el[i]是指在不推迟真个工期的前提下,活动ai必须开始的最晚时间。若活动ai由有向边表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后。

案例:

原始AOE网:
5.png

事件的最早发生时间:ve[k]
从源点向终点方向计算
ve[0] = 0
ve[1] = ve[0] + a0 = 0 + 4 = 4
ve[2] = max( ve[0] + a1, ve[1] + a2 ) = max(0 + 3, 4 + 2)= 6
ve[3] = max(ve[1] + a4, ve[2] + a3) = max(4 + 6, 3 + 4) = 10

事件的最迟发生时间:vl[k]
从终点向源点方向计算
vl[3] = ve[3] = 10
vl[2] = vl[3] - a3 = 10 - 4 = 6
vl[1] = min(vl[3] - a4, vl[2] - a2) = min(10-6, 6-2) = 4
vl[0] = min(vl[2] - a1, vl[1] - a0) = min(4-4, 4-2) = 0

活动的最早发生时间:ee[i]
共有五个活动:
ee[0] = ve[0] = 0
ee[1] = ve[0] = 0
ee[2] = ve[1] = 4
ee[3] = ve[2] = 6
ee[4] = ve[1] = 4

活动的最迟发生时间:el[i]
el[0] = v[1] - a0 = 4 - 4 = 0
el[1] = vl[2] - a1 = 6 - 3 = 3
el[2] = vl[2] - a2 = 6 - 2 = 4
el[3] = vl[3] - a3 = 10 - 4 = 6
el[4] = vl[3] - a4 = 10 - 6 = 4

活动的最早开始时间和最晚开始时间相等,则说明该活动时属于关键路径上的活动,即关键活动。
经过比较,得出关键活动有:a0, a2, a3, a4,画出示意图如下:

6.png

该AOE网有两条关键路径。所以,通过此案例也可以发现,一个AOE网的关键路径可能有多条。

代码实现

/* 邻接矩阵结构 */
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;

/* **************************** */

/* 关于AOE网图的存储代码段-Begin */
//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;

}


//2.将邻近矩阵转化成邻接表
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 adjList[i].in=0;
        (*GL)->adjList[i].data=G.vexs[i];
        //将边表置为空表
        (*GL)->adjList[i].firstedge=NULL;
    }

    //建立边表
    for(i=0;iadjvex=j;
                e->weight=G.arc[i][j];
                //将当前顶点上的指向的结点指针赋值给e
                e->next=(*GL)->adjList[i].firstedge;
                //将当前顶点的指针指向e
                (*GL)->adjList[i].firstedge=e;
                (*GL)->adjList[j].in++;

            }
        }
    }
}
/* 关于AOE网图的存储代码段-End! */
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(sizeof(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;
}
//求关键路径, GL为有向网,则输出G的各项关键活动;
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; jnumVertexes;j++)
    {
        for (e = GL->adjList[j].firstedge; e; e = e->next) {
            //获取与j连接的顶点;
            k = e->adjvex;
            //ete 就是表示活动  的最早开工时间, 是针对这条弧来说的.而这条弧的弧尾顶点Vk 的事件发生了, 它才可以发生. 因此ete = etv[k];
            ete = etv[j];
            //lte 表示活动 的最晚开工时间, 但此活动再晚也不能等Vj 事件发生才开始,而是必须在Vj 事件之前发生. 所以lte = ltv[j] - len.
            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);
            }
        }
    }
    
}

你可能感兴趣的:(从零开始养成算法·篇十八:图的应用之拓扑排序与关键路径)