1. 拓扑排序
1.1 拓扑排序简介
有⼀个表示⼯程的有向图中, ⽤顶点表示活动, 用弧表示活动之间的优先关系,这样有向图为顶点表示活动的⽹,我们称为AOV网(Activity On Vertex Network).
如下图1.1.1所示:
图中活动的有限集为:
C1~C2~C3~C4~C5
或者为:
C2~C1~C3~C4~C5
设G=(V,E)是一个具有n个顶点的有向图, V中的顶点序V1,V2,.....,Vn。若满足从顶点Vi到Vj有一条路径,则在顶点序列中Vi必须在Vj之前,则我们称这样的顶点序列为一个拓扑序列。
所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。
构造过程拓扑序列会产生2个结果:
①. 如果此网中的全部顶点都被输出,则说明它是不存在环(回路)的AOV网。
②. 如果输出的顶点数少了,哪怕仅少了一个,也说明这个网存在环(回路路),不是AOV⽹。
1.2 拓扑排序算法
对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的定点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点后AOV网中不存在入度为0的顶点位置。
在拓扑排序中过程中,需要删除顶点,所以用邻接表会更加方便,因此我们需要为AOV网建立一个邻接表,并在顶点表结点中加入一个入度域in,结点如下图所示:
拓扑算法基础代码如下:
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXEDGE 20
#define MAXVEX 14
/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Status;
/*邻接矩阵结构 */
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;
在这个算法实现过程中,我们需要借助一个数据结构——栈,来帮助我们解决避免每次查找时, 都要去遍历AOV图中的顶点表去查找有没有入度为0的顶点。
①. 创建一个栈(stack),⽤来存储入度in为0的顶点序号;
②. 遍历AOV图中顶点表,判断入度为0的顶点全部入栈;
下面我们按照图1.2.1构建一个邻接表,为了省事,我们直接通过代码生成。
①. 首先我们通过代码构建一个AOV网图:
/*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;
}
②. 将AOV网图借助邻近矩阵转换成邻接表结构:
/*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++;
}
}
}
}
有了上面的邻接表数据后,我们进行拓扑排序,具体如下:
/*拓扑排序. 若AOV网图无回路则输出拓扑排序的序列并且返回状态值1,若存在回路则返回状态值0*/
/*拓扑排序:解决的是一个工程能否顺序进行的问题!*/
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;
}
分析算法:将入度为0的顶点入栈的时间复杂度为O(n), 而之后的while 循环,每个顶点进一次栈,并且出一次栈。入度减1, 则共执行了e次. 那么整个算法的时间复杂度为O(n+e)。
测试代码如下:
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, 拓扑排序!\n");
MGraph G;
GraphAdjList GL;
int result;
CreateMGraph(&G);
CreateALGraph(G,&GL);
result=TopologicalSort(GL);
printf("result:%d\n",result);
return 0;
}
上面的代码输出结果为:3 -> 1 -> 2 -> 6 -> 0 -> 4 -> 5 -> 8 -> 7 -> 12 -> 9 -> 10 ->13 -> 11
这并不是唯一的拓扑排序结果.
2. 关键路径
2.1 关键路径概述
在⼀个表示工程的带权的有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表表示活动的网,我们称之为AOE网(Activity On Edge Network)
没有入边的顶点称为始点或源点;
没有出边的顶点称为终点或汇点;
由于一个工程, 总有一个开始,一个结束。所以正常情况下,AOE⽹只有一个源点和一个汇点。
如下图2.1.1,就是一个AOE网,其中V0就是起点(源点),表示一个工程的开始,V9就是终点(汇点),表示一个工程的结束。顶点V0、V1......V9表示事件,弧
AOV网和AOE网都是用来对工程建模的,但是他们还是有很大的不同,主要体现在AOV网是顶点表示的活动的网,它只描述活动之间的制约关系,而AOE网使用边表示活动的网,边上的权值表示活动的持续时间。
例如,造一辆汽车。我们需要先造各种各样的零部件,最终才能完成汽⻋的组装。那么假如造一个轮⼦需要0.5天的时间, 造⼀个发动机需要3天时间,造一个车底盘需要2天时间,其他零部件需要3天时间,全部零部件集中到一处需要0.5天,组装成汽车需要2天时间。
那么这个工程的AOV网和AOE网分别为:
相关名词:
路径上各个活动所持续的时间之和称为路径长度
从源点到汇点具有最大的路径叫关键路径
在关键路径上的活动叫关键活动
2.2 关键路径算法
在了解算法之前,我们先来了解一些参数概念:
① 事件的最早发生时间etv(elrliest time of vertex):即顶点Vk的最早发生时间。
② 事件的最晚发生时间ltv(latest tie of vertex):即顶点Vk的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超过此时间将会延误整个工期。
③ 活动的最早开工时间ete(earliest time of edge):即弧ak的最早发生时间。
④ 活动的最晚开工时间lte(latest time of edge):即弧ak的最晚发生时间,也就是不推迟工期的最晚开工时间。
我们是由①和②求得③和④,然后再根据ete[k]是否与lte[k]相等来判断ak是否是关键活动。
2.2.1 事件最早发生时间etv计算
求事件的最早发生时间etv的过程,就是从头至尾找拓扑序列的过程,因此在求解关键路径之前,需要调用一次拓扑排序算法来计算etv和拓扑序列表,首先声明几个全局变量。
int *etv,*ltv; /* 事件最早发生时间和最迟发生时间数组,全局变量 */
int *stack2; /* 用于存储拓扑序列的栈 */
int top2; /* 用于stack2的指针*/
其中stack2用来存储拓扑序列,方便后面求解关键路径使用。
求解etv和拓扑序列表代码如下:
//拓扑排序
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;
}
2.2.2 事件最晚发生时间ltv及关键路径计算
事件的最晚发生时间ltv(latest tie of vertex):即顶点Vk的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超过此时间将会延误整个工期。
- 首次出栈的gettop = 9,但由于V9 没有弧表, 则没有相关的ltv更新,即ltv[9] = 27.
- 当gettop = 8, 由图可知,与V8连接的顶点有V9,弧长<8,9>为3,所以V8的最晚发生时间为ltv[9]减去弧长<8,9>,得到的值与ltv[8]的值比较,取二者较小的值,即ltv[8] = 24.
- 当gettop = 7, 由图可知,与V7连接的顶点有V8,弧长<7,8>为5,所以V7的最晚发生时间为ltv[8]减去弧长<7,8>,得到的值与ltv[7]的值比较,取二者较小的值,即ltv[7] = 19.
- 当gettop = 5, 由图可知,与V5连接的顶点有V7,弧长<5,7>为6,所以V5的最晚发生时间为ltv[7]减去弧长<5,7>,得到的值与ltv[5]的值比较,取二者较小的值,即ltv[5] = 13.
- 当gettop = 6, 由图可知,与V6连接的顶点有V9,弧长<6,9>为2,所以V6的最晚发生时间为ltv[9]减去弧长<6,9>,得到的值与ltv[6]的值比较,取二者较小的值,即ltv[6] = 25.
- 当gettop = 4, 由图可知,与V4连接的顶点有V6和V7,弧长分别为<4,6>为9、<4,7>为4,所以V4的最晚发生时间为ltv[6]减去弧长<4,6>的值与ltv[7]减去弧长<4,7>的值分别与ltv[4]的值比较,取较小值更新ltv[4],最终ltv[4] = 15.
关于ete以及lte计算:
ete表示活动
lte表示活动
最终计算代码如下:
//求关键路径, 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);
}
}
}
}
时间复杂度计算:
拓扑排序的时间复杂度:O(n+e);
初始化ltv时间复杂度:O(n);
计算ltv的时间复杂度:O(n+e);
计算ete/lte时间复杂度: O(n+e);
所以最终求得关键路径的时间复杂度为: O(n+e)