数据结构(十一)----图的应用

目录

一.最小生成树

1.Prim算法(普里姆)

2.Kruskal算法(克鲁斯卡尔):

二.最短路径(BFS算法)

1.单源最短路径

(1)BFS算法(无权图)

(2)Dijkstra算法(带权图,无权图)

2.各顶点间的最短路径

(1)Floyd(带权图,无权图)

三.有向无环图(DAG)

1.算术表达式

2.拓扑排序

•逆拓扑排序

3.关键路径


一.最小生成树

连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。

一个带权的连通图,可能有多棵生成树,在所有生成树中,所有边权值之和最小的生成树就是最小生成树。

•最小生成树可能有多个,但边的权值之和总是唯一且最小的。

•最小生成树的边数 =顶点数 -1。砍掉一条则不连通,增加一条边则会出现回路。

•如果一个连通图本身就是一棵树,则其最小生成树就是它本身。

•只有连通图才有生成树,非连通图只有生成森林。

所以在最小生成树中研究的对象是带权的连通的无向图

1.Prim算法(普里姆)

从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。

具体过程如下:

从P出发,代价最小的新顶点为学校,那么把学校纳入生成树。

继续将最小的新顶点纳入生成树,这里有两个代价为4的路径,都可以选择。

依次类推,直到将所有顶点纳入生成树中。根据路径不同,生成树也不同,但是最小代价一定是一样的。

时间复杂度为O(|V|^2)

从代码的角度看:

需要两个数组,一个数组记录该结点是否在生成树中,一个数组记录该结点到树的代价,如果没有与树直连,那么代价为∞。如下图,v0为初始结点,其他结点都不在生成树中:

并且其他结点到v0的代价为:

1.循环遍历所有个结点,找到lowCast最低的,且还没加入树的顶点(3号顶点),将其加入生成树中。

2.再次循环遍历,更新还没加入的各个顶点的lowCast值。由于3号结点的加入,其他结点到生成树的代价更新为:

依次类推,从v0开始,每一轮都会选择新的顶点,将其放到生成树中,n个顶点就需要n-1轮处理,而每一轮的处理又需要两次循环遍历,即遍历所有的结点,并且处理他们的lowcost(到生成树的代价),所以每一轮的处理时间复杂度为O(2n),经过n-1轮处理,那么时间复杂度为O(n^2),即O(v^2)。

//用邻接矩阵存储图
#define INFINITY INT_MAX    //最大值∞
#define MAX_VERTEX_NUM 20    //最大顶点个数
typedef enum{DG,DN,UDG,UDN} GraphKind;    //{有向图,有向网,无向图,无向网}
typedef struct ArcCell{VRType adj;    //权值InfoType *info;    //该弧相关信息的指针
}ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];typedef struct{VertexType vexs[MAX_VERTEX_NUM];    //顶点向量AdjMatrix arcs;    //邻接矩阵int vexnum,arcnum;    //顶点数和弧数GraphKind kind;    //图的种类
}MGraph;
bool isJoin[MAX_VERTEX_NUM]; // 记录顶点是否已加入最小生成树int Locate(MGraph G,VertexType u){int k=-1;for(int i=0;i<G.vexnum;i++){if(G.vexs[i]==u){return i;}}return k;
}void MiniSpanTree_PRIM(MGraph G,VertexType u){//从第u个顶点出发,构造网G的最小生成树T,输出T的各条边//记录从顶点集U到V-U的代价最小的边的辅助数组memset(isJoin, false, sizeof(isJoin));    //初始化isJoin数组struct{VerTexType adjvex;VRType lowcost;}closedge[MAX_VERTEX_NUM];k=Locate(G,u);for (j=0;j<G.vexnum;++j)if(j!=k)    closedge[j]={u,G.arc[k][j].adj};closedge[k].lowcost=0;for(i=1;i<G.vexnum;i++){k=minimum(closedge);    //求出T的下一个结点:第k顶点printf("%d %d\n", closedge[k].adjvex, G.vexs[k]);isJoin[k]=true;    //标记为已被访问closedge[k].lowcost=0;for(j=0;j<G.vexnum;++j)if(!isJoin[j] && G.arc[k][j].adj<closedge[j].lowcost)    //将新结点并入U后重新选择最小边close[j]={G.vexs[k],G.arcs[k][j].adj};}}
2.Kruskal算法(克鲁斯卡尔):

每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通

具体过程如下:

P到学校权值最小,并且他们之前并没有连通,所以将两点连通。

从剩下的边中挑选权值最小的边,如图为2,由于渔村和矿场之前没有连通,所以将这两点连通。

依次类推,执行完这一步时,最小代价的边是4,但由于这条边的两头已经连通了,所以不选这条边。

依次在剩余的边当中,权值最小,且还没有连通的两点为农场和P城。

到此为止,所有的结点都连通了,这棵最小代价树最小代价也是15。

时间复杂度为O(|E|log_{2}|E|)

从代码角度看:

1.初始时,将各条边按权值排序。

2.接着,从上至下检查每条边的两个顶点是否连通(是否属于同一集合),若不连通,那么将两个点相连。

3.下一条是v3和v5,但由于两个顶点已经属于同一个集合,所以跳过这条边,依次类推。

图中有E条边,那么总共需要进行E轮处理,在每一轮处理中,需要通过并查集判断两个顶点是否属于同一个集合,时间复杂度为O(log_{2}E),那么E轮处理,总的时间复杂度为O(|E|log_{2}|E|)

总结:

1.Prim算法每次是选择一个顶点,Kruskal算法每次是选择一条边。

2.Prim算法的时间复杂度只和顶点的个数有关,时间复杂度为O(|V|^2),Kruskal算法的时间复杂度只和边有关,时间复杂度为O(|E|log_{2}|E|)。所以Prim算法适用于边稠密图,也就是|E|(边)比较大的场景,Kruskal算法适用于边稀疏图,也就是|E|(边)比较小的场景。

二.最短路径(BFS算法)

看下面的例子, “G港”是个物流集散中心,经常需要往各个城市运东西怎么运送距离最近?这就是单源最短路径问题。各个城市之间也需要互相往来,相互之间怎么走距离最近?这就是每对顶点间的最短路径

1.单源最短路径
(1)BFS算法(无权图)

无权图可以视为一种特殊的带权图,只是每条边的权值都为1。

在BFS算法中,1号顶点,6号顶点与2号顶点的距离为1

5,3,7与2的距离为2

4,8与2的距离为3

求2号顶点到其他顶点的最短路径:

#define INFINITY INT_MAX    //最大值∞void BFS_MIN_Distance(Graph G,int u){//d[i]表示从u到i结点的最短路径for(i=0;i<G.vexnum;i++){d[i] = INT_MAX;    //初始化路径长度path[i]=-1;    //最短路径从哪个顶点过来}d[u] =0;visited[u]=TRUE;EnQueue(Q,u);while(!isEmpty(Q)){DeQueue(Q,u);for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){if(!visited[w]){d[w]=d[u]+1;    //路径长度+1path[w]=u;    //最短路径应从u到wvisited[w]=TRUE;    //设为以访问标记EnQueue(Q,w);    //顶点w入队}//if}//for}//while
}

具体过程如下:
从2号顶点出发,则将2号顶点出队,并且将2号顶点邻接的点的路径长度设为1(d[w]=d[u]+1),也就是将d[1],d[6]=1,并且将path[1],path[6]=2,表示最短路径从2过来

 

其余依次类推:

通过path数组可知,2到8的最短路径为:8<--7<--6<--2

(2)Dijkstra算法(带权图,无权图)

BFS算法只能用于无权图,对于带权图则不再适用。例如下图,若要求G港到R城的最短路径,用BFS算法得到的最短路径是10,其实从P城再到R城才是最短路径7。

Dijkstra算法就能解决这个问题,首先需要初始化三个数组:

接着循环遍历所有结点,找到还没确定最短路径,且dist 最小的顶点Vi(final=false,dis[i]最小),令final[i]=ture。在v1,v2,v3,v4四个顶点中,最小的值是5。

检查所有与v4相邻的顶点,若其final值为false,则更新dist和path的信息。由于v0到v4的路径是5,v4到v1的距离为3,所以找到了从v0到v1的更短的路径,即5+3=8。

现在final=false的顶点当中,路径最小的是7,所以现在处理v3,将final[3]=true。检查可以从v3过去的顶点,若其final=false,则更新dist和path的信息。

继续以上步骤,最后得到:

v0到v2的最短(带权)路径长度为dist[2]=9

通过path[]可知,v0到v2的最短路径:v2<--v1<--v4<--v0

时间复杂度为O(n^2)即O(|V|^2),因为从final=false的顶点中找到dist最小的顶点,并将其设为final[i]=false,时间复杂度为O(n)。并且需要更新与该点相邻的final=false的点的信息,若图为邻接矩阵存储,要找到与他相邻的所有的点,就是要扫描该点对应的行,时间复杂度为O(n),所以每一轮处理需要的时间复杂度为O(n)+O(n)=2O(n),总共需要n-1轮处理,所以算法的时间复杂度为O(|V|^2)。

Dijkstra算法和Prim算法的区别: 

Prim算法数组记录的是顶点加入到目前生成树的最小代价,dist数组记录的是当前顶点到指定顶点的最短路径的值。

若带权图中有负权值的边,那用Dijkstra算法找到的路径可能不是最短路径,所以Dijkstra算法不适用于有负权值的带权图。但是Floyd算法可以用于负权图。

void DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D){//求有向网G的v0顶点到其余顶点v的最短路径P[v]及其带权长度D[v]//若P[v][w]为TRUE,则w为从v0到v当前求得最短路径的顶点for(v=0;v<G.vexnum;++v){final[v]=FALSE;    D[v]=G.arcs[v0][v];for(w=0;w<G.vexnum;++w)    p[v][w]=FALSE;    //设空路径if(D[v]<INFINITY){P[v][v0]=TRUE;p[v][v]=TRUE;}}D[v0]=0;    final[v0]=TRUE;    //初始化,v0顶点属于S集//开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集中 for(i=1;i<G.vexnum;++i){min=INFINITY;    //当前所知离v0顶点的最近距离for(w=0;w<G.vexnum;++w)if(!final[w]){if(D[w]<min){v=w;min=D[w];}    //w顶点离顶点更近final[v]=TRUE;for(w=0;w<G.vexnum;++w)if(!final[w] && (min+G.arcs[v][w])<D[w])){D[w]=min+G.arcs[v][w];P[w]=P[v];    P[w][w]=TRUE;    //P[w]=P[v]+P[w]}}
}
2.各顶点间的最短路径
(1)Floyd(带权图,无权图)

从v2到v1的路径为∞

1.若允许从v0中转,则v2到v1的路径可变为v2-->v0-->v1,距离为11:

也就是:

广义化则变为:

更新A和path后,矩阵为:

2.若允许在v0,v1中转,在更新的矩阵的基础上,扫描所有的元素,只有A[0][2]满足条件:

更新后的矩阵如下:

3.若允许在v0,v1,v2中转,在更新的矩阵的基础上,扫描所有元素,只有A[1][0]满足条件

更新后的矩阵如下:

所以从A^{(-1)}path^{(-1)}开始,经过n轮递推,得到A^{(n-1)}path^{(n-1)}

void FLOYD(MGraph G,PathMatrix &P[],DistanceMatrix &D){//P[v][w]表示最短路径,D[v][w]表示带权路径长度//若P[v][w][u]为TRUE,则u是从v到w当前求得的最短路径for(v=0;v<G.vexnum;++v)for(w=0;w<G.vexnum;++w){D[v][w]=G.arcs[v][w];for(u=0;u<G.vexnum;++u)    P[v][w][u]=FALSE;if(D[v][w]<INFINITY){P[v][w][u]=TRUE;p[v][w][w]=TRUE; }}for(u=0;u<G.vexnum;++u)for(v=0;v<G.vexnum;++v)for(w=0;w<G.vexnum;++w)if(D[v][u]+D[u][w]<D[v][w]){D[v][w]=D[v][u]+D[u][w];for(i=0;i<G.vexnum;++i)P[v][w][i]=P[v][u][i] || P[u][w][i];    //更新最短路径}
}

 由上面的代码可得,时间复杂度为O(n^3)即O(|V|^3)。

再考虑更加复杂的例子:

前面的步骤相同,直接考虑以v2作为中转:

可以更新的最短路径如下:

注意:A[0][3]的路径可以转化为A[0][2]+A[2][3],其实A[2][3]之间是没有直接路径的,但是在v2进行中转 是在v0,v1也允许中转 的基础上进行的,之前已经更新了从v2到v3的路径,即:

v2->v1->v3

所以A[0][2]+A[2][3]实际经过的路径是v0->v2->v1->v3

以此类推,最终得到:

如何通过矩阵找完成的路径,例如,找v0到v4的最短路径,p[0][4]=3,v0->v3->v4:

由于p[0][4]不为-1,所以中间还有其他顶点。

依次看v0->v3和v3->v4,由于v0->v3的path[0][3]=2,说明中间还经过了其他结点:

v0->v2->v3。

p[3][4]=-1,说明中间没有经过任何顶点。

继续拆解v0->v2->v3,v0->v2的path[0][2]=-1,中间不经过其他顶点,v2->v3的path[2][3]=1,说明中间还经过了其他顶点,v2->v1->v3

继续拆解v2->v1->v3,v2->v1的path[2][1]=-1,中间不经过其他顶点,v1->v3的path[1][3]=-1,中间不经过任何顶点,至此拆解完毕,最后得到从v0到v4的完整路径为:
v0->v2->v1->v3->v4

Floyd算法可以用于负权图,例子如下:

但是Floyd 算法不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。例如下图,可以观察到,如果回路走的次数越多,那么其带权路径会越来越小。

总结:

其实也可用 Dijkstra 算法求所有顶点间的最短路径,重复|V|次即可(也就是分别以图的各个顶点作为源顶点,求源顶点到达其他结点的最短路径),总的时间复杂度也是O(|V|^3)

三.有向无环图(DAG)

1.算术表达式

若一个有向图中不存在环,则称为有向无环图,简称DAG图(Directed Acyclic Graph),常用作描述表达式。

之前我们用树表示算术表达式,对于下面的式子所画的树,红绿两个子树是相同的。

我们可以丢弃重复的部分,节省存储空间,如下图所示,这个图就是一个有向无环图:

如何完全丢弃重复的部分呢?

1.把各个操作数不重复地排成一排

2.标出各个运算符地生效顺序

3.按生效顺序加入运算符:

第1,2个生效的是两个"+"号:

第3个生效的是*号,需要用到+号产生的运算结果,所以比+号更上一层:

依次类推:

4.由于各个操作数是不重复,只需要检查操作符即可。所以自底向上逐层检查同层的运算符是否可以合并。

在第一层中,第一个“+”左右是a,b  右边三个"+"左边都是c,d,所以可以合并。

依次类推,得到最简的算数表达式如下:

注:为什么只检查同层呢,拿第一层为例,第一层的操作符左右两边肯定是操作数,而第二层的操作符,左右两边肯定有复合的操作数,所以第一层操作数和第二层操作数是不可能合并的。

再举一个例子:

若改变操作符的生效顺序,那么有向无环图的形态是不唯一的。

2.拓扑排序

AOV网(Activity On Vertex NetWork,用顶点表示活动的网)。用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边<V_{i}V_{j}>表示活动V_{i}必须先于活动V_{j}进行。

拓扑排序的实现如下:

① 从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
② 从网中删除该顶点和所有以它为起点的有向边。
③ 重复①和②)直到当前的AOV网为空当前网中不存在无前驱的顶点为止

每个AOV网可能有一个或多个拓扑排序序列:例如下图,可以先准备厨具,也可以先买菜。

若一个图中有回路,那么这个图就不能拓扑排序,例如下图,执行到某一步时会发现,当前所有顶点的入度都大于0,拓扑排序无法继续进行。

拓扑排序的代码如下:

#define MaxVertexNum 100    //图中顶点数目的最大值
typedef struct ArcNode{    //边表结点int adjvex;    //该弧所指向的顶点的位置struct ArcNode *nextarc;     //指向下一条弧的指针//InfoType info;    //网的边权值   
}ArcNode;typedef struct VNode{    //顶点表结点VertexType data;    //顶点信息ArcNode *firstarc;    //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];typedef struct{AdjList vertices;    //邻接表int vexnum,arcnum;    //图的顶点数和弧数
}Graph;    //Graph是以邻接表存储的图类型bool TopologicalSort(Graph G){InitStack(S);    //初始化栈,存储入度为0的顶点for(int i=0;i<G.vexnum;i++)if(indegree[i]==0)Push(S,i);    //将所有入度为0的顶点进栈int count=0;    //计数,记录当前已经输出的顶点数while(!IsEmpty(S)){    //栈不空,则存在入度为0的顶点Pop(S,i);print[count++]=i;for(p=G.vertices[i].firstarc;p;p->nextarc){//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈Sv=p->adjvex;if(!(--indegree[v]))Push(S,v);    //入度为0,则入栈}}if(count<G.vexnum)    //在上图中,count=5(结点数),表示拓扑排序成功return false;  //拓扑失败,有向图有回路elsereturn true;    //拓扑排序成功
}

由于该算法中,每一个顶点都会被处理一次,每一条边也会被遍历一次,所以时间复杂度为O(|V|+|E|),若采用邻接矩阵,把所有的边都遍历一遍,其实就是扫描整个邻接矩阵,所以时间复杂度为O(|V|^2)。

•逆拓扑排序

逆拓扑排序和拓扑排序的步骤是一样的,只是每一次删除的是出度为0的顶点

具体过程如下:

① 从AOV网中选择一个没有后继(出度为0)的顶点并输出。

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

③ 重复①和②直到当前的AOV网为空。

对于逆拓扑排序,不同的存储结构对逆拓扑排序的时间复杂度影响很大。因为删除某一个顶点,也需要删除指向这个顶点的边。若采用邻接表,要找到指向一个顶点的边,就意味着需要遍历整个表。而采用邻接矩阵,只需要遍历该点对应的列即可。

可以使用逆邻接表,邻接表中,每个结点的边表保存的是从这个结点出来的边的信息,而逆邻接表中,每个结点的边表是指向结点的边的信息。

可以用DFS算法实现逆拓扑排序

void DFSTraverse(Graph G){    //对图进行深度优先遍历for(v=0;v<G.vexnum;++v)visited[v]=FALSE;for(v=0;v<G.vexnum;++v)if(!visited[v])DFS(G,v);
}//用栈递归
void DFS(Graph G,int v){    //从顶点v出发,深度优先遍历图Gint w;visited[v]=TRUE;for(w=FirstNeighbor(G,v);w>=0;w=Neighbor(G,v,w))if(!visited[w]){DFS(G,w);}print(v);    //各个顶点的信息在退栈时输出
}

和拓扑排序相同,拓扑排序和逆拓扑排序序列可能是不唯一的,若图中有环,则不存在拓扑排序/逆拓扑排序序列。

如何检测图中是否有环呢?可以增加一个一个数组path[ ]来记录从起点到当前节点的路径。如果发现下一个节点已经被访问过,并且不是当前节点的上一个节点,那么就说明存在回路。

改进代码如下:

#define MAX_VERTEX_NUM 100  // 定义最大顶点数int visited[MAX_VERTEX_NUM];  // 访问标志数组
int path[MAX_VERTEX_NUM];     
// 路径数组,用来记录上一个访问的结点,例如path[1]=0表示结点1的上一个结点是0void DFSTraverse(Graph G){    //对图进行深度优先遍历for(v=0;v<G.vexnum;++v){visited[v]=FALSE;path[v]=-1;    //初始化路径数组}for(v=0;v<G.vexnum;++v)if(!visited[v])DFS(G,v);
}//用栈递归
void DFS(Graph G,int v){    //从顶点v出发,深度优先遍历图Gint w;visited[v]=TRUE;for(w=FirstNeighbor(G,v);w>=0;w=Neighbor(G,v,w)){if(!visited[w]){    //有未被访问过的邻接点path[w]=v;DFS(G,w);        }else if(path[v]!=w){    //下一个结点已经被访问过,并且不是当前访问的结点的上一个结点,则存在环路return;    }    }print(v);
}

3.关键路径

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)。上面说的AOV网是用顶点表示活动,而AOE网使用边表示活动,而顶点表示的是一个个事件。

AOE网有以下两个性质:

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

② 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。另外,有些活动是可以并行进行的(例如上图,洗番茄和打鸡蛋是可以同时进行的,而洗番茄和切番茄不能同时进行)。

AOE网的相关概念:

① 在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;也仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。

② 从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动
完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长。

③ 

事件v_{k}最早发生时间ve(k)--决定了所有从vk开始的活动能够开工的最早时间。

活动a_{i}最早开始时间e(i)--指该活动弧的起点所表示的事件的最早发生时间。

④ 

事件v_{k}最迟发生时间vl(k)--它是指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。

活动a_{i}的最迟开始时间--它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。例如下图,切番茄这一活动必须在4 - 3=1这个时刻开始,否则会推迟整个工程完成的进度。

⑤将活动最早开始时间和活动最晚开始时间结合起来看:

活动a_{i}最早开始时间e(i)--指该活动弧的起点所表示的事件的最早发生时间。

活动a_{i}的最迟开始时间--它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。

观察上图可以发现,只有打鸡蛋这个活动可以拖到2时刻开始,其余活动是不能拖延的。

活动a_{i}的时间余量d[i]=l[i]-e[i],表示在不增加完成整个工程所需总时间的情况下,活动a_{i}可以拖延的时间。

活动的时间余量为零,则说明该活动必须要如期完成,d(i)=0即l(i)=e(i)的活动a_{i}关键活动。由关键活动组成的路径就是关键路径

(1)求所有事件的最早发生时间ve()

源点的最早发生时间是0,其余顶点的最早发生时间是前驱顶点的最早发生时间+活动消耗的时间

ve(源点)=0

ve(k)=Max{ve(j)+Weight(v_{j}+v_{k})},vj为vk的任意前驱。

例如下图,v3的最早发生时间是2,v4有两个直接前驱。若从v2过来,那么v4最早发生时间是3+2=5。若从v3过来,v4的最早发生时间为2+4=6。只有所有指向该顶点的活动都结束后,这个时间才能发生,所以取更大的值,v4的最早发生时间是6。

依次类推得到所有事件的最早发生时间:

(2)求所有时间的最迟发生时间vl()

按逆拓扑排序序列,依次求各个顶点的vl(k):

vl(6)=ve(6)=8        //终点所允许的最迟发生时间和其最早发生时间是一样的。

vl(k)=Min{vl(j)-Weight(v_{k},v_{j})},v_{j}v_{k}的任意后继,例如下图,v5的最迟发生时间等于:

v6(后继)的最迟发生时间-1=7

如下图,v2有两个直接后继,若以v5为后继活动,v2的最迟发生时间7-3=4。若以v4为后继活动,v4的最迟发生时间是6-2=4。取最小值,所以v2的最迟发生时间是4。

依次类推: 

(3)求所有活动的最早发生时间e()

若边<v_{k},v_{j}>表示活动a_{i},则有e(i)=ve(k),也就是每个活动的最早发生时间就是这个活动的弧尾所连的事件的最早发生时间。

得到所有活动的最早发生时间如下:

(4)求所有活动的最迟发生时间l()

若边<v_{k},v_{j}>表示活动a_{i},则有l(i)=vl(j)-Weight(v_{k},v_{j}),也就是这条弧所指向的事件的最晚发生时间 - 这条弧的权值。例如下图,活动a4的最迟发生时间就是7-3=4

依次类推,所有活动的最迟发生时间:

(5)求时间余量 d(i)=l(i)-e(i)

只需要用活动的最晚发生时间-活动的最早发生时间。

a2,a5,a7活动的时间余量都为0,所以这几个活动是关键活动,这几个活动对应的边就是关键路径。

关键活动、关键路径的特性

•若关键活动耗时增加,则整个工程的工期将增长
•缩短关键活动的时间,可以缩短整个工程的工期

•当缩短到一定程度时,关键活动可能会变成非关键活动

例如下图,切番茄是关键活动,若将切番茄的时间缩短为1,那么整个工程的工期缩短,也就是4,但是若继续压缩切番茄的时间,那么他就不是关键活动了,因为他没办法让工程缩短,打鸡蛋和炒菜的时间最少需要4。关键活动变为打鸡蛋和炒菜。

所以不是关键路径上的活动越压缩,整个工程的工期就会越提前。

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

如上图所示,v1->v2->v3->v4是一条关键路径,v1->v3->v4是一条关键路径。加快包含在所有关键路径上的关键活动,例如炒菜,才能缩短工期。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/8927.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不走寻常路!酷开科技不断升级酷开系统满足消费者日益增长的需求

在科技日新月异的今天&#xff0c;人们对生活品质的要求越来越高。为此&#xff0c;酷开科技不断升级酷开系统&#xff0c;以满足消费者日益增长的需求。为了让消费者体验更好的服务&#xff0c;在酷开系统中设立了酷开会员&#xff0c;满足消费者的更多需求。丰富的特权和定制…

STL学习笔记

1 基本概念 1.1 STL STL(Standard Template Library,标准模板库)STL从广义上分为: 容器(container) 算法(algorithm) 选代器(iterator)容器和算法之间通过迭代器&#xff08;看作指针&#xff09;进行无缝连接STL 几乎所有的代码都采用了横板类或者模板函数 1.2 容器 STL容器…

图片如何压缩到500kb以下?3步完成图片压缩

在日常生活和工作中&#xff0c;经常需要处理各种图片&#xff0c;而有时候图片文件过大&#xff0c;不仅占用了大量的存储空间&#xff0c;还可能影响文件的传输速度和加载速度。因此&#xff0c;如何将图片压缩到500kb以内成为了许多人的需求&#xff0c;普通的图片压缩可能没…

使用Docker安装Whistle Web Debugging Proxy

大家好&#xff0c;继续给大家分享如何使用docker来安装Whistle Web Debugging Proxy&#xff0c;关于Whistle Web Debugging Proxy的介绍和使用&#xff0c;大家可以参考下面文章&#xff0c;希望本文能够给大家的工作带来一定帮助。 Whistle Web Debugging Proxy介绍及使用 …

vue+lodop实现web端打印标签功能

背景&#xff1a;项目要求在web端连接标签打印机&#xff0c;打印收件人信息 lodop打印插件地址&#xff1a;Lodop和C-Lodop官网主站 在项目中使用 1、去官网下载lodop包下载中心 - Lodop和C-Lodop官网主站 windows系统直接下载windows32版的就可以 2、解压安装 点击CLodop…

SpringCloud Config 分布式配置中心

SpringCloud Config 分布式配置中心 概述分布式系统面临的——配置问题ConfigServer的作用 Config服务端配置Config客户端配置 可以有一个非常轻量级的集中式管理来协调这些服务 概述 分布式系统面临的——配置问题 微服务意味着要将单体应用中的业务拆分成一个个字服务&…

python如何整体缩进

python自带编辑器的缩进和取消缩进快捷键&#xff1a; 整体缩进 Ctrl【 整体取消缩进 Ctrl】 pycharm编辑器的缩进和取消缩进快捷键&#xff1a; 整体缩进&#xff1a; tab 整体取消缩进&#xff1a; tabshift

HDMI ARC功能详解及应用介绍

一、HDMI HDMI(High-Definition Multimedia Interface&#xff0c;高清多媒体接口)&#xff0c;是一种专用的音频/视频接口&#xff0c;用于发送未压缩的视频数据和压缩/未压缩的音频数据。HDMI是模拟视频标准的数字替代品。HDMI视频和音频信号传输通道采用了TMDS&#xff08;T…

【经验总结】Vue2中的全局变量(store

需求场景 需要在vue中存储一个可变的&#xff0c;可读写的全局变量在不同的js、页面中均可调用和读写 技术&#xff1a;使用vue的store 用法总结 一、定义变量 1、找到vue的/src/store路径&#xff0c;在modules文件夹下创建文件&#xff08;这里便于测试创建demo.js&…

51单片机入门:DS1302时钟

51单片机内部含有晶振&#xff0c;可以实现定时/计数功能。但是其缺点有&#xff1a;精度往往不高、不能掉电使用等。 我们可以通过DS1302时钟芯片来解决以上的缺点。 DS1302时钟芯片 功能&#xff1a;DS1302是一种低功耗实时时钟芯片&#xff0c;内部有自动的计时功能&#x…

SpringBoot启动流程源码解析

目录 一、SpringApplication构造方法解析 1. web应用类型 2. BootstrapRegistryInitializer 3. ApplicationContextInitializer 4. ApplicationListener 5. 推断Main方法所在类 二、SpringApplication.run(String... args)方法解析 1.创建DefaultBootstrapContext 2.获…

订单超时自动取消的实践方案

1、定时任务方案 方案流程&#xff1a; 每隔 30 秒查询数据库&#xff0c;取出最近的 N 条未支付的订单。 遍历查询出来的订单列表&#xff0c;判断当前时间减去订单的创建时间是否超过了支付超时时间&#xff0c;如果超时则对该订单执行取消操作。 定时任务方案工程实现相…

【Vue】vue中将 html 或者 md 导出为 word 文档

原博主 xh-htmlword文档 感谢这位大佬的封装优化和分享&#xff0c;亲测有用&#xff01;可以去看大佬&#x1f447;的说明&#xff01; 前端HTML转word文档&#xff0c;绝对有效&#xff01;&#xff01;&#xff01; 安装 npm install xh-htmlword导入 import handleEx…

远动通讯屏的作用

远动通讯屏的作用 远动通讯屏有时有称为调度数据网柜&#xff0c;远动通讯屏具体干啥作用&#xff1f;远动通讯屏是以计算机为基础的生产过程与调度自动化系统&#xff0c;可以对现场的运行设备进行监视和控制、以实现数据采集、设备测量、参数调节以及各类信号报警等各项功能。…

用webui.sh安装报错No module named ‘importlib.metadata‘

安装sdweb报错&#xff0c;出现No module named importlib.metadata&#xff1a; glibc version is 2.35 Cannot locate TCMalloc. Do you have tcmalloc or google-perftool installed on your system? (improves CPU memory usage) Traceback (most recent call last):File…

堆的基本操作(c语言实现)

1.堆的基本操作 1.1定义堆 typedef int HPDataType;//堆中存储数据的类型typedef struct Heap {HPDataType* a;//用于存储数据的数组int size;//记录堆中已有元素个数int capacity;//记录堆的容量 }HP;1.2初始化堆 然后我们需要一个初始化函数&#xff0c;对刚创建的堆进行初…

【C语言】路漫漫其修远兮,深入[指针]正当下

一. 指针初步 1.概念定义 地址&#xff1a;我们在内存中开辟空间时&#xff0c;为了方便后续访问&#xff0c;每个数据有确切的地址。 指针&#xff1a;指向数据的地址&#xff0c;并将其地址储存在指针变量中。 2.基本运算符 • 取地址操作符&#xff08;&&#xff09; …

【强化学习入门】基于DDPG的强化学习控制器设计

最近在看控制领域研究热门–强化学习相关的东西&#xff0c;跟着matlab官方强化学习教程一边看一边学&#xff0c;感觉入门门槛略高&#xff0c;需要补很多机器学习相关的知识&#xff0c;高数概率论那些&#xff0c;摸索了个把月感觉现在只大概会用&#xff0c;原理啥的还没搞…

进口家装水管十大品牌哪家好,弗锐德为您推荐进口家装水管领先十大品牌

水管作为家装隐蔽工程之一&#xff0c;选对一款优质的水管是至关重要的&#xff0c;毕竟好的水管能够保证家庭后续几十年的用水安全和健康。今天&#xff0c;小编就和大家说说进口家装水管十大品牌哪家好&#xff1f; 目前国内进口家装水管具有知名度和消费者认可的品牌有&…

自制一个3D打印的移动终端——T3rminal

T3rminal是我过去几个月一直在努力开发的一个CyberDeck&#xff0c;并希望将其开源。 我从不同设备如Decktility、YARH和其他项目中获得了灵感。 你可以在我的Github上协助并关注该项目&#xff1a;https://github.com/crazycaleb2008/T3rminal/tree/main/3D%20Models 材料 …