《大话数据结构》——学习笔记(图)

图的定义

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通过表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合(有穷非空),E是图G中边的集合(可以为空)

这里写图片描述

图是一种较线性表和树更加复杂的数据结构,在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关

各种图定义

无向边: 若顶点vivj 之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi ,vj )来表示,如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)

有向边: 若从顶点vivj 的边有方向,则称这条边为有向边,也称为弧(Arc),用有序偶<vi ,vj >来表示,vi 称为弧尾(Tail),vj 称为弧头(Head),如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)

在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图

在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图,含有n个顶点的无向完全图有n×(n1)2 条边

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图,含有n个顶点的有向完全图有nx(n-1)条边

有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight),这种带权的图通常称为网(Network)

假设有两个图G=(V,{E})和G’=(V’,{E’}),如果V’∈V且E’∈E,则称G’为G的子图(Subgraph)

图的顶点与边间关系

对于无向图G=(V,{E}),如果边(v,v’)∈E,则称顶点v和v’互为邻接点(Adjacent),即v和v’相邻接,边(v,v’)依附(incident)于顶点v和v’,或者说(v,v’)与顶点v和v’相关联。顶点v的度(Degree)是和v相关联的边的数目,记为TD(v)

对于有向图G=(V,{E}),如果弧<v,v’>∈E,则称顶点v邻接到顶点v’,顶点v’邻接自顶点v。弧<v,v’>和顶点v,v’相关联,以顶点为头的弧的数目称为v的入度(InDegree),记为ID(v),以v为尾的弧的数目称为v的出度(OutDegree),记为OD(v);顶点v的度为TD(v)=ID(v)+OD(v)

路径的长度是路径上的边或弧的数目

第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle),序列中顶点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路,称为简单回路或简单环,如下左图为简单环,右图不是简单环

<img shuju_46>

连通图

在无向图G中,如果从顶点v到顶点v’有路径,则称v和v’是连通的,如果对于图中任意两个顶点vivj ∈E,vivj 都是连通的,则称G是连通图(Connected Graph)

无向图中的极大连通子图称为连通分量,它强调:

  • 要是子图
  • 子图要是连通的
  • 连通子图含有极大顶点数
  • 具有极大顶点数的连通子图包含依附于这些顶点的所有边

在有向图G中,如果对于每一对vivj ∈V、vivj ,从vivj 和从vjvi 都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量,如下图所示,左图并不是强连通图,右图是强连通图,且是左图的极大强连通子图,即是左图的强连通分量

<img shuju_047>

一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边,如下图

<img shuju_048>

如果一个有向图恰有一个顶点的入度为0(根结点),其余顶点的入度均为1,则是一棵有向树。一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧,如下图

<img shuju_049>

图的存储结构

邻接矩阵

将图分成顶点和边或弧两个结构来存储,顶点不分大小、主次,所以用一个一维数组来存储,而边或弧由于是顶点与顶点之间的关系,所以用二维数组(称为邻接矩阵)来存储

设图G有n个顶点,则邻接矩阵是一个nxn的方阵,定义为:

shuju_050

<img shuju_051>

无向图的边数组是一个对称矩阵

有了这个矩阵,可以很容易地知道图中的信息

  • 很容易判断任意两顶点是否有边无边
  • 某个顶点的度就是这个顶点vi 在邻接矩阵中第i行(或第i列)的元素之和,如v1 的度就是1+0+1+0=2
  • 求顶点vi 的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点

有向图的边数组不是一个对称矩阵,有向图样例如下

<img shuju_052>

网图是每条边上带有权的图,设图G是网图,有n个顶点,则邻接矩阵是一个nxn的方阵,定义为:

<img shuju_053>

这里Wij 表示(vi ,vj )或<vi ,vj >上的权值,∞表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值

<img shuju_054>

缺点: 对于边数相对于顶点较少的图,这种结构是对存储空间的极大浪费

邻接表

数组与链表相结合的存储方法称为邻接表(Adjacency List)

邻接表的处理方法:

  • 图中顶点用一个一维数组存储,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息
  • 图中每个顶点vi 的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi 的边表,有向图则称为顶点vi 作为弧尾的出边表

<img shuju_055>

<img shuju_056>

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息

<img shuju_057>

缺点: 对于有向图来说,邻接表关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况

十字链表

把邻接表与逆邻接表结合起来就组成了十字链表(Orthogonal List)

顶点表结点结构如下表所示

data firstin firstout

其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点

边表结点结构如下表所示

tailvex headvex headlink taillink

其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边,如果是网,还可以再增加一个weight域来存储权值

<img shuju_058>

对于v0 来说,它有两个顶点v1v2 的入边,因此v0 的firstin指向顶点v1 的边表结点中headvex为0的结点,如图中①。接着由入边结点的headlink指向下一个入边顶点v2 ,如图中的②。对于顶点v1 ,它有一个入边顶点v2 ,所以它的firstin指向顶点v2 的边表结点中headvex为1的结点,如图中③。顶点v2v3 也是同样有一个入边顶点,如图中④和⑤

十字链表的好处就是把邻接表和逆邻接表整合在了一起,这样既容易找到一vi 为尾的弧,也容易找到以vi 为头的弧,因而容易求得顶点的出度和入度

邻接多重表

在无向图的应用中,如果关注的重点是顶点,那么邻接表是不错的选择,但如果更关注边的操作,比如对已访问过的边做标记,删除某一条边等操作,就意味着,需要找到这条边的两个边表结点进行操作,这还是比较繁琐的

对十字链表的边表结点的结构进行一些改造,就可以避免这个问题

重新定义的边表结点结构如下所示

ivex ilink jvex jlink

其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标,ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边,这就是邻接多重表结构

<img shuju_059>

首先连线的①②③④就是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同,接着,由于顶点v0 的(v0 ,v1 )边的邻边有(v0 ,v3 )和(v0 ,v2 )。因此⑤⑥的连线就是满足指向下一条依附于顶点v0 的边的目标,注意ilink指向的结点的jvex一定要和它本身的ivex的值相同。同样,连线⑦就是指(v1 ,v0 )这条边,它是相当于顶点v1 指向(v1 ,v2 )边后的下一条,v2 有三条边依附,所以在③之后就有了⑧⑨。连线⑩的就是顶点v3 在连线④之后的下一条边

邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个节点表示,而在邻接多重表中只有一个结点,这样对边的操作就方便多了,若删除上图的(v0 ,v2 )这条边,只需要将⑥⑨的链接指向改为^即可

边集数组

边集数组是由两个一维数组构成,一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成

<img shuju_061>

边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高,因此它更适合对边依次进行处理操作,而不适合对顶点相关的操作

图的遍历

从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历(Traversing Graph)

深度优先遍历

深度优先遍历(Depth_First_Search),也称为深度优先搜索,简称DFS,类似于树的前序遍历

<img shuju_062>

从顶点A开始,在没有碰到重复顶点的情况下,始终是向右手边走,当走到H处发现没有通道没走过,此时一层层向上返回,把没有走过的通道标记,如D->I,直到返回顶点A

对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此需要O(n2 )的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高

广度优先遍历

广度优先遍历(Breadth_First_Search),又称广度优先搜索,简称BFS,类似于树的层序遍历

先将下图变形成层序结构,变形原则是顶点A放置在第一层,让与它有边的顶点B、F为第二层,再让与B和F有边的顶点C、I、G、E为第三层,再将这四个顶点有边的D、H放在第四层

<img shuju_064>

图的深度优先遍历与广度优先遍历算法在时间复杂度上是一样的,不同之处在于对顶点访问的顺序不同

深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况

最小生成树

把构造连通网的最小代价生成树称为最小生成树(Minimum Cost Spanning Tree)

找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法

普里姆(Prim)算法

定义: 假设N=(P,{E})是连通网,TE是N上最小生成树中边的集合。算法从U={u0 }(u0 ∈V)
,TE={}开始。重复执行下述操作:在所有u∈U,v∈V-U的边(u,v)∈E中找一条代价最小的边(u0 ,v0 ),
并入集合TE,同时v0 并入U,直至U=V为止。此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树

普里姆(Prim)算法是以某顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树的

此算法的时间复杂度为O(n2 )

示例:

<img shuju_065>

上图中左图G有9个顶点,它的arc二维数组如上图右图所示,数组中用65535来代表∞

用普里姆算法解析过程如下:

/* Prim算法生成最小生成树 */

void MiniSpanTree_Prim(MGraph G){
    int min, i ,j, k;
    int adjvex[MAXVEX]; /* 保存相关顶点下标 */
    int lowcost[MAXVEX]; /* 保存相关顶点间边的权值 */
    lowcost[0] = 0; /* 初始化第一个权值0,即v0加入生成树 */
    adjvex[0] = 0; /* 初始化第一个顶点下标为0 */
    for(i = 1; i < G.numVertexes; i++){
        lowcost[i] = G.arc[0][i]; /* 将v0顶点与之有边的权值存入数组 */
        adjvex[i] = 0; /* 初始化都为v0的下标 */
    }
    for(i = 1; i < G.numVertexes; i++){
        min = INFINITY; /* 初始化最小权值为∞ */
        j = 1; k = 0;
        while(j < G.numVertexes){ /* 循环全部顶点 */
            if(lowcost[j] != 0 && lowcost[j] < min){
                /* 如果权值不为0且权值小于min */
                min = lowcot[j]; /* 则让当前权值成为最小值 */
                k = j; /* 将当前最小值的下标存入k */
            }
            j++;
        }
        printf("(%d, %d)", adjvex[k], k); /* 打印当前顶点边中权值最小边 */
        lowcost[k] = 0; /* 将当前顶点的权值设置为0,表示此顶点已经完成任务 */
        for(j = 1; j < G.numVertexes; j++){
            /* 循环所有顶点 */
            if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j]){
                /* 若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值 */

                lowcost[j] = G.arc[k][j]; /* 将较小权值存入lowcost */
                adjvex[j] = k; /* 将下标为k的顶点存入adjvex */
            }
        }
    }

}

最终构造过程如下图所示

<img shuju_066>

克鲁斯卡尔(Kruskal)算法

定义: 假设N={V,{E}}是连通网,则令最小生成树的初始状态为只有n个顶点而无边的非连通图T={V,{}},图中每个顶点自成一个连通分量,在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止

克鲁斯卡尔(Kruskal)算法是以边为目标去构建,因为权值是在边上,直接去找最小权值的边来构建生成树也是很自然的想法,只不过构建时要考虑是否会形成环路而已

示例:

<img shuju_067>

上图将左图转化成右图的边集数组,并且对它们按权值从小到大排序

克鲁斯卡尔算法代码如下:

/* Kruskal算法生成最小生成树 */
void MiniSpanTree_Kruskal(MGraph G) {
    int i, n, m;
    Edge edges[MAXEDGE]; /* 定义边集数组 */
    int parent[MAXVEX]; /* 定义一数组用来判断边与边是否形成环路 */
    /* 此处省略将邻接矩阵G转化为边集数组edges并按权由小到大排序的代码 */
    for(i = 0; i < G.numVertexes; i++){
        parent[i] = 0; /* 初始化数组值为0 */
    }
    for(i = 0; i < G.numEdges; i++){
        /* 循环每一条边 */
        n = Find(parent, edges[i].begin);
        m = Find(parent, edges[i].end);
        if(n != m){
            /* 假如n与m不等,说明此边没有与现有生成树形成环路 */
            parent[n] = m; /* 将此边的结尾顶点放入下标为起点的parent中,表示此顶点已经在生成树集合中 */
            printf("(%d, %d) %d", edges[i].begin, edges[i].end, edges[i].weight);
        }
    }
}

int Find(int *parent, int f){
    /* 查找连线顶点的尾部下标 */
    while(parent[f] > 0){
        f = parent[f];
    }
    return f;
}

此算法的Find函数由边数e决定,时间复杂度为O(e ),而外面有一个for循环e次,所以克鲁斯卡尔算法的时间复杂度为O(ee )

对比两个算法,克鲁斯卡尔算法主要是针对边展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些

最短路径

在网图和非网图中,最短路径的含义是不同的,由于非网图它没有边上的权值,所谓的最短路径,其实就是指两顶点之间经过的边数最少的路径;而对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且称路径上的第一个顶点是源点,最后一个顶点是终点

迪杰斯特拉(Dijkstra)算法

这是一个按路径长度递增的次序产生最短路径的算法,它的思路大体是这样的,从源点开始,一步步求出从源点到终点间所有顶点的最短路径,过程都是基于已经求出的最短路径的基础上,求出更远顶点的最短路径

迪杰斯特拉的算法如下:

#define MAXVEX 9 
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX]; /* 用于存储最短路径下标的数组 */
typedef int ShortPathTable[MAXVEX]; /* 用于存储到各点最短路径的权值和 */
/* Dijkstra算法,求有向网G的v0顶点到其余顶点v最短路径P[v]及带权长度D[v],P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和 */
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx * P, ShortPathTable *D){
    int v, w, k, min;
    int final[MAXVEX]; /* final[w]=1表示求得顶点v0到vw的最短路径 */
    for(v = 0; v < G.numVertexes; v++){
        /* 初始化数据 */
        final[v] = 0; /* 全部顶点初始化为未知最短路径状态 */
        (*D)[v] = G.matirx[v0][v]; /* 将与v0点有连线的顶点加上权值 */
        (*P)[v] = 0; /* 初始化路径数组P为0 */
    }
    (*D)[v0] = 0; /* v0至v0路径为0 */
    final[v0] = 1; /* v0至v0不需要求路径 */
    /* 开始主循环,每次求得v0到某个v顶点的最短路径 */
    for(v = 1; v < G.numVertexes; v++){
        min = INFINITY; /* 当前所知离v0顶点的最近距离 */
        for(w = 0; w < G.numVertexes; w++){
            /* 寻找离v0最近的顶点 */
            if(!final[w] && (*D)[w] < min){
                k = w;
                min = (*D)[w]; /* w顶点离v0顶点更近 */
            }
        }
        final[k] = 1; /* 将目前找到的最近的顶点置为1 */
        for(w = 0; w < G.numVertexes; w++){
            /* 修正当前最短路径及距离 */
            /* 如果经过v顶点的路径比现在这条路径的长度短的话 */
            if(!final[w] && (min + G.matirx[k][w] < (*D)[w])){
                /* 说明找到了更短的路径,修改D[w]和P[w] */
                (*D)[w] = min + G.matirx[k][w]; /* 修改当前路径长度 */
                (*P)[w] = k;
            }
        }
    }
}

通过迪杰斯特拉(Dijkstra)算法解决了从某个源点到其余各顶点的最短路径问题,从循环嵌套可以很容易得到此算法的时间复杂度为O(n2 ),求所有顶点到所有顶点的时间复杂度为O(n3 )

弗洛伊德(Floyd)算法

如下图,要求出所有顶点到所有顶点的最短路径

<img shuju_070>

先定义两个二维数组D1 [3][3]和P1 [3][3],D1 代表顶点到顶点的最短路径权值和的矩阵。P1 代表对应顶点的最小路径的前驱矩阵。通过分析,v1 ->v0 ->v2 得到D1 [1][0]+D1 [0][2]=3 要小于 v1 ->v2 得到的D1 [1][2]=5,所以修正D1 [1][2]=D1 [1][0]+D1 [0][2],P1 [1][2]也修改为当前中转的顶点v0 的下标0,也就是说

D0 [v][w]=min{D1 [v][w], D1 [v][0] + D1 [0][w]}

示例:

<img shuju_069>

弗洛伊德算法如下:

typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
/* Floyd算法,求网图G中各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w] */
void ShortestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D){
    int v, w, k;
    for(v = 0; v < G.numVertexes; ++v){
        /* 初始化D与P */
        for(w = 0; w < G.numVertexes; ++w){
            (*D)[v][w] = G.matirx[v][w]; /* D[v][w]值即为对应点间的权值 */
            (*P)[v][w] = w; /* 初始化P */
        }
    }
}

for(k = 0; k < G.numVertexes; ++k){
    for(v = 0; v < G.numVertexes; ++v){
        for(w = 0; w < G.numVertexes; ++w){
            if((*D)[v][w] > (*D)[v][k] + (*D)[k][w]){
                /* 如果经过下标为k顶点路径比原两点间路径更短,将当前两点间权值设为更小的一个 */
                (*D)[v][w] = (*D)[v][k] + (*D)[k][w];
                (*P)[v][w] = (*P)[v][k]; /* 路径设置经过下标为k的顶点 */
            }
        }
    }
}

如果需要求所有顶点至所有顶点的最短路径问题,弗洛伊德(Floyd)算法是不错的选择

拓扑排序

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

设G={V,E}是一个具有n个顶点的有向图,V中的顶点序列v1v2 … 满足若从顶点vivj 有一条路径,则在顶点序列中顶点vi 必须在顶点vj 之前,则称这样的顶点序列为一个拓扑序列

拓扑排序就是对一个有向图构造拓扑序列的过程,构造时会有两个结果,如果此网的全部顶点都被输出,则说明它是不存在环(回路)的AOV网;如果输出顶点数少了,说明这个网存在环(回路),不是AOV网

一个不存在回路的AOV网,可以应用在各种各样的工程或项目的流程图中,满足各种应用场景的需要

拓扑排序算法

对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删除此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止

先为AOV网建立一个邻接表,如下图所示

<img shuju_071>
<img shuju_072>

拓扑排序算法实现如下:

/* 拓扑排序,若GL无回路,则输出拓扑排序序列并返回OK,若有回路返回ERROR */
Status TopologicalSort(GraphAdjList GL){
    EdgeNode *e;
    int i, k, gettop;
    int top = 0; /* 用于栈指针下标 */
    int count = 0; /* 用于统计输出顶点的个数 */
    int *stack; /* 建栈存储入度为0的顶点 */
    stack = (int *)malloc(GL->numVertexes * sizeof(int));
    for(i = 0; i < GL->numVertexes; i++){
        if(GL->adjList[i].in == 0){
            stack[++top] = i; /* 将入度为0的顶点入栈 */
        }
    }
    while(top != 0){
        gettop = stack[top--]; /* 出栈 */
        printf("%d -> ", GL->adjList[gettop].data); /* 打印此顶点 */
        count++; /* 统计输出顶点数 */
        for(e = GL->adjList[gettop].firstedge; e; e = e->next){
            /* 对此顶点弧表遍历 */
            k = e->adjList;
            if(!(--GL->adjList[k].in)){
                /* 将k号顶点邻接点的入度减1 */
                stack[++top] = k; /* 若为0则入栈,以便于下次循环输出 */
            }
        }
    }
    if(count < GL->numVertexes){
        /* 如果count小于顶点数,说明存在环 */
        return ERROR;
    } else {
        return OK;
    }
}

整个过程就是先找到入度为0的顶点,对该顶点对应的弧链表进行遍历,将弧链表中的顶点的入度减一,不断循环这个过程,最终输出所有的顶点

<img shuju_073>

对一个具有n个顶点e条弧的AOV网来说,将入度为0的顶点入栈的时间复杂度为O(n),而之后的while循环中,每个顶点进一次栈,出一次栈,入度减1的操作共执行了e次,所以整个算法的时间复杂度为O(n+e)

关键路径

在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称为AOE网(Activity On Edge Network)

AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点,正常情况下,AOE网只有一个源点一个汇点

AOV网是顶点表示活动的网,它只描述活动之间的制约关系,而AOE网是用边表示活动的网,边上的权值表示活动持续的时间,因此AOE网是要建立在活动之间制约关系没有矛盾的基础之上,再来分析完成整个工程至少需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动等问题

<img shuju_074>

把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫关键活动,如开始->发动机完成->部件集中到位->组装完成就是关键路径,路径的长度为5.5

只有缩短关键路径上的关键活动时间才可以减少整个工期长度

关键路径算法原理

只需要找到所有活动的最早开始时间和最晚开始时间,并且比较它们,如果相等就意味着此活动是关键活动,活动间的路径为关键路径,如果不等,则就不是

为此,需要定义如下几个参数:

  • 1.事件的最早发生时间etv(earliest time of vertex):即顶点vk 的最早发生时间
  • 2.事件的最晚发生时间ltv(latest time of vertex):即顶点vk 的最晚发生时间,也就是每个顶点对应的事件最晚需要开始时间,超出此时间将会延误整个工期
  • 3.活动的最早开工时间ete(earliest time of edge):即弧ak 的最早发生时间
  • 4.活动的最晚开工时间lte(latest time of edge):即弧ak 的最晚发生时间,也就是不推迟工期的最晚开工时间

由1和2可以求得3和4,然后再根据ete[k]是否与lte[k]相等来判断ak 是否是关键活动

关键路径算法

将AOE网转化为邻接表结构,如下图

这里写图片描述

求事件的最早发生时间etv的过程,就是从头至尾找拓扑序列的过程,因此,在求关键路径之前,需要先调用一次拓扑序列算法的代码来计算etv和拓扑序列列表

这里写图片描述

由此可以得出计算顶点vk 即求etv[k]的最早发生时间的公式是:

这里写图片描述

在计算ltv时,其实是把拓扑序列倒过来进行的,因此可以得出计算顶点vk 即求ltv[k]的最晚发生时间的公式是:

这里写图片描述

求关键路径的算法代码如下:

/* 求关键路径,GL为有向图,输出GL的各项关键活动 */
void CriticalPath(GraphAdjList GL){
    EdgeNode *e;
    int i,gettop,k,j;
    int ele,lte; /* 声明活动最早发生时间和最迟发生时间变量 */
    TopologicalSort(GL); /* 求拓扑序列,计算数组etv和stack2的值 */
    ltv=(int *)malloc(GL->numVertexes*sizeof(int)); /* 事件最晚发生时间 */
    for(i=0; i<GL->numVertexes; i++){
        ltv[i]=etv[GL->numVertexes-1]; /* 初始化ltv */
    }
    while(top2 != 0){
        gettop = stack2[top2--]; /* 将拓扑序列出栈,后进先出 */
        for(e = GL->adjList[gettop].firstedge; e; e=e->next){
            /* 求各顶点事件的最迟发生时间ltv值 */
            k=e->adjvex;
            if(ltv[k]-e->weight < ltv[gettop]){
                /* 求各顶点事件最晚发生时间ltv */
                ltv[gettop] = ltv[k] - e->weight;
            }
        }
        for(j=0; j<GL->numVertexes; j++){
            /* 求ete,lte和关键活动 */
            for(e = GL->adjList[j].firstedge; e; e=e->next){
                k=e->adjvex;
                ete=etv[j]; /* 活动最早发生时间 */
                lte=ltv[k]-e->weight; /* 活动最迟发生时间 */
                if(ete == lte){
                    /* 两者相等即在关键路径上 */
                    printf("<v%d,v%d> length: %d , ", GL->adjList[j].data,GL->adjList[k].data,e->weight);
                }
            }
        }
    }
}

/* 拓扑排序,用于关键路径计算 */
Status TopologicalSort(GraphAdjList GL){
    EdgeNode *e;
    int i,k,gettop;
    int top=0; /* 用于栈指针下标 */
    int count=0; /* 用于统计输出顶点的个数 */
    int *stack; /* 建栈将入度为0的顶点入栈 */
    stack=(int *)malloc(GL->numVertexes * sizeof(int));
    for(i=0; i<GL->numVertexes; i++){
        if(0 == GL->adjList[i].in){
            stack[++top]=i;
        }
    }
    top2=0; /* 初始化为0 */
    etv=(int *)malloc(GL->numVertexes * sizeof(int)); /* 事件最早发生时间 */
    for(i=0; i<GL->numVertexes; i++){
        etv[i]=0; /* 初始化为0 */
    }
    stack2=(int *)malloc(GL->numVertexes*sizeof(int)); /* 初始化 */
    while(top != 0){
        gettop=stack[top--];
        count++;
        stack2[++top2]=gettop; /* 将弹出的顶点序号压入拓扑序列的栈 */

        for(e=GL->adjList[gettop].firstedge; e; e=e->next){
            k=e->adjvex;
            if(!(--GL->adjList[k].in)){
                stack[++top]=k;
            }
            if((etv[gettop]+e->weight > etv[k])){
                /* 求各顶点事件最早发生时间值 */
                etv[k]=etv[gettop] + e->weight;
            }
        }
    }
    if(count < GL->numVertexes){
        return ERROR;
    } else {
        return OK;
    }
}

etv和ltv的数组求得如下图

<img shuju_079>

如果etv[1]=3,ltv[1]=7,表示的意思是如果时间单位是天的话,哪怕v1 这个事件在第7天才开始,也可以保证整个工程的按期完成,可以提前v1 事件的开始时间,但最早也只能在第3天开始

最终的关键路径如下图所示

<img shuju_080>

求关键路径算法的时间复杂度为O(n+e)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章