1. 最小生成树
1.1 最小生成树的概念
- 生成树:连通图的生成树是包含图中全部顶点的一个极小连通子图。 若图中顶点数为 n,则它的生成树含有 n-1 条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
- 最⼩⽣成树(最⼩代价树):对于一个带权连通无向图
,生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spannino-Tree,MST).。
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的。
- 最小生成树的边数 =顶点数 -1。砍掉一条则不连通,增加一条边则会出现回路。
- 如果一个连通图本身就是一棵树,则其最小生成树就是它本身。
- 只有连通图才有生成树,非连通图只有生成森林。
求最小生成树的两种方法
1.2 Prim算法(普里姆):
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。时间复杂度: O(V2)适合用于边稠密图。核心思想:贪心算法
同一顶点开始生成的最小生成树可能也不一样,但是最小代价是一样的
算法实现:
Prim 算法的实现思想:
1.初始:从V0开始,标记各节点是 否已加⼊树isJoin,各节点加⼊树 的最低代价,lowCost
2. 第1轮:循环遍历所有个结点,找 到lowCost最低的,且还没加⼊树 的顶点将该顶点加入树,再次循环遍历,更新还没加⼊的 各个顶点的lowCost值
3. 重复1,2,从V0开始,总共需要 n-1 轮处理,每⼀轮处理:循环遍历所有个结 点,找到lowCost最低的,且还没 加⼊树的顶点。 再次循环遍历,更新还没加⼊的 各个顶点的lowCost值,
每⼀轮时间复 杂度O(2n),总时间复杂度 O(n2),即O(|V|2)
void Prim(G, T)
{
// T为空;
// U = {w};
while((V-U)! = NULL)
{
设(u,v)为让u属于U,v属于(V-U)对最短边
T = T U {(u,v)}; //边入树
U = U U {v}; //顶点入树
}
}
//辅助数组:
isJoin[vexNum]; //标记各节点是否已加入树
lowCost[vexNum]; //各节点加入树的最小代价 != 权值,每次并入新节点后都需要更新
1.3 Kruskal算法(克鲁斯卡尔):
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。时间复杂度: O(|E|log|E|)适合用于边稀疏图。
算法实现:
1. 初始:将各条边按权值排序
2.第1轮:检查第1条边的两个顶点是否 连通(是否属于同⼀个集合) 不连通,则连起来
2.第i轮:检查第i条边的两个顶点是否 连通(是否属于同⼀个集合)不连通,则连起来,已连通,则跳过
共执⾏ e 轮,每轮判断两个顶点是 否属于同⼀集合,需要 O(log2e) 总时间复杂度 O(elog2e)
void Kruskal(v, T)
{
T = v;
numS = n; //连通分量数
while(numS>1)
{
从E中选取权值最小的边(u,v);
if(v和u属于不同连通分量)
{
T = T U {(v,u)}; //边入树
numS--;
}
}
}
2. 最短路径问题
2.1 无权图的单源最短路径问题——BFS算法
⽆权图可以视为⼀种特殊的带权图,只是每条边的权值都为1
从2出发寻找无权图的单源最短路径
算法实现:
使用 BFS算法求无权图的最短路径问题,需要使用三个数组
d[]
数组用于记录顶点 u 到其他顶点的最短路径。path[]
数组用于记录最短路径从那个顶点过来。
visited[]
数组用于记录是否被访问过。
在visit⼀个顶点时,修改其最短路径⻓度 d[ ] 并在 path[ ] 记录前驱结点
代码实现:
#define MAX_LENGTH 2147483647 //地图中最大距离,表示正无穷
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(i=0; i<G.vexnum; i++){
visited[i]=FALSE; //初始化访问标记数组
d[i]=MAX_LENGTH; //初始化路径长度
path[i]=-1; //初始化最短路径记录
}
InitQueue(Q); //初始化辅助队列
d[u]=0;
visites[u]=TRUE;
EnQueue(Q,u);
while(!isEmpty[Q]){ //BFS算法主过程
DeQueue(Q,u); //队头元素出队并赋给u
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){
if(!visited[w]){
d[w]=d[u]+1;
path[w]=u;
visited[w]=TRUE;
EnQueue(Q,w); //顶点w入队
}
}
}
}
2.2 带权图的单源最短路径问题——Dijkstra算法
2.2.1 相关概念背景
带权路径⻓度——当图是带权图时,⼀条路径上所有边的权值之和,称为该路径的带权路径⻓度
- BFS算法的局限性:BFS算法求单源最短路径只适⽤于⽆权图,或所有边的权值都相同的图。
- Dijkstra算法能够很好的处理带权图的单源最短路径问题,但不适⽤于有负权值的带权图。
2.2.2 算法实现:
使用 Dijkstra算法求最短路径问题,需要使用三个数组:
final[]
数组用于标记各顶点是否已找到最短路径。dist[]
数组用于记录各顶点到源顶点的最短路径长度。path[]
数组用于记录各顶点现在最短路径上的前驱。
1. 初始:从V0开始,初始化三个数组信息
2. 第1轮:循环遍历所有结点,找到还没确定最短 路径,且dist 最⼩的顶点Vi,令final[i]=ture,检查所有邻接⾃ Vi 的顶点,若其 final 值为false, 则更新 dist 和 path 信息
3.重复过程2,n-1轮处理,直到所有顶点的final 值为true.并更新完成
4. 使⽤数组信息
代码实现:
#define MAX_LENGTH = 2147483647;
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(int i=0; i<G.vexnum; i++){ //初始化数组
final[i]=FALSE;
dist[i]=G.edge[u][i];
if(G.edge[u][i]==MAX_LENGTH || G.edge[u][i] == 0)
path[i]=-1;
else
path[i]=u;
final[u]=TREE;
}
for(int i=0; i<G.vexnum; i++){
int MIN=MAX_LENGTH;
int v;
// 循环遍历所有结点,找到还没确定最短路径,且dist最⼩的顶点v
for(int j=0; j<G.vexnum; j++){
if(final[j]!=TREE && dist[j]<MIN){
MIN = dist[j];
v = j;
}
}
final[v]=TREE;
// 检查所有邻接⾃v的顶点路径长度是否最短
for(int j=0; j<G.vexnum; j++){
if(final[j]!=TREE && dist[j]>dist[v]+G.edge[v][j]){
dist[j] = dist[v]+G.edge[v][j];
path[j] = v;
}
}
}
}
时间复杂度: O(n2)即O(|V|2)
2.3 单源、有负权边最短路径问题——贝尔曼福特算法(Bellman-Ford)
2.3.1 基本原理:
逐遍的对图中每一个边去迭代计算起始点到其余各点的最短路径,执行N-1遍,最终得到起始点到其余各点的最短路径。(N为连通图结点数)
- 实现通过m次迭代求出从起点到终点不超过m条边构成的最短路径
2.3.2 与迪杰斯特拉算法的区别:
1. 迪杰斯特拉算法是借助贪心思想,每次选取一个未处理的最近的结点,去对与他相连接的边进行松弛操作;贝尔曼福特算法是直接对所有边进行N-1遍松弛操作。
2. 迪杰斯特拉算法要求边的权值不能是负数;贝尔曼福特算法边的权值可以为负数,并可检测负权回路。
优于Dijkstra的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高。时间复杂度是O(nm)
名词解释:
1. 松弛操作:不断更新最短路径和前驱结点的操作。
2. 负权回路:绕一圈绕回来发现到自己的距离从0变成了负数,到各结点的距离无限制的降低,停不下来
注意:如果图中有负权回路的话,最短路就不一定存在了,转一圈长度就会减少,因此我们可以转无穷多圈,转无穷多圈总长度就会变成负无穷,出圈的话还是负无穷。所以说图中存在负权回路的话,从1号点到n号点的距离就会变成负无穷,就不存在了。所以能求出来最短路的话,图中是没有负权回路的。
而Bellman-ford算法是可以判断图中存不存在负权回路。首先上面的迭代次数是有实际意义的,比如我们迭代了k次,那么我们求的最短距离就是从1号点经过不超过k条边走到n号点的最短距离。所以在第n次迭代的时候又更新了某些边的话,就说明路径中一定存在环,并且是负权回路。因为第n次迭代在不存在负权回路的情况下是遍历到第n号点了,后面是没有点了,如果还能更新,说明路径中存在回路,而且是负权回路。
一般找负环是不用Bellman-ford算法,一般是用SPFA算法——Bellman—Ford算法的队列优化
2.3.3 为啥能求最短路?为啥迭代次数有意义?
首先,Bellman算法的核心是松驰,和Dijsktra算法不一样,Djikstra算法是松驰+贪心(,其实质就是在问相应边对面的顶点————“你能够被改进(更短)吗?”)
最短路算法的本质,都是在研究 松驰的顺序!通过不断的松驰,最终求得每个顶点的最短路
- Dijkstra松驰顺序,是依次松驰距离源点距离最短的未被处理过的点与之相连的顶点。是以一种贪心策略进行松驰的。这种特点导致,一旦某个顶点被处理过(即对与它相连的顶点进行松驰),那么后面该顶点自己被松驰,但是与它相连的顶点不能因为它的松驰而松驰,导致出现不准确的结果。(当边全为正,是不会出现这种情况,因为在松驰与该顶点相连的顶点时,这种算法已经保证了该点已经被松驰到极限)。
- Bellman算法的核心就是松驰,没有贪心策略,也使它的时间复杂度比较高。因为它是单纯的松驰。首先我们要明白的是:如果处于第n层的节点,在它上一层的即n-1层所以节点的dist已经确定为最终真实值,那么通过一次遍历,第n层节点的dist也能被确定为最终真实值。第一次迭代,获得的信息是:与源点相邻点的真正dist(第二层节点),(其他点的可能仍为无穷大,或者为松驰一次状态);第二次循环,因为第二层的信息已经完全掌握,此次迭代是能确定第三层节点(从源点出发,经过2条边)的点的真实最短距离。(由于遍历的过程中,只完全掌握了第一层,其他节点的dist不能完全确定为最终的dist);如此循环,遍历n-1次,第n层的节点已经遍历完,至此,所有节点的dist都最终确定了(解释了为啥能求最短路)。
经过上面的分析,可以得出,bellman的松驰顺序是的策略是,暴力遍历,无脑松驰。
串联问题
串联问题一般发生在求解有边数限制的最短路问题中
在遍历的过程中,虽然说第二层的节点的dist可能任然为初始化的正无穷,但是由于第一层的更新和第二层的更新是同时的,很有可能更新完某个第一层节点,恰好后面去更新与它相连的第二层节点,那么该第二层节点的dist由于第一层节点的更新也更新了(如果该第二层节点同时也是处于第一层位置,
可以发现,如果我们没有备份上一次的dist数组的话,限制从1出发不超过1条边到3最短距离本应该是3,但变成了2。内层循环只迭代了一次,但是在更新的过程中会发生”串联”
防止串联,其实就是防止在第k次循环,更新k+1层节点时,由于k+1层节点的更新和确定,以k+1更新后的结果为基础松驰了与之相连的下一层的某个节点。!
假设每次迭代,遍历所有边,遍历边的顺序如下:
1→2, 1→3, 2→3
遍历完第一条边dist[2] = 1,遍历完第二条边dist[3] = 3,遍历第三条边,由于1→2的dist已经确定,在掌握这个信息的前提下,发生串联,dist[3]可以直接松驰,更新为dist[3] = 2,但这不是我们想要的答案。我们想要的是:迭代k次,得到从源点出发,不超过k条边的最短路。
怎么保证不发生串联呢?我们保证更新的时候只用上一次循环的结果就行。所以我们先备份一下。备份之后backup数组存的就是上一次循环的结果,我们用上一次循环的结果来更新距离。所以我们这样写dist[b]=min(dist[b],backup[a]+w)来更新距离,而不是dist[b]=min(dist[b],dist[a]+w),这样写就会发生上面说的”串联”现象。
假如我们现在是第k次迭代,那么backup保留的是第k-1次迭代后获得的信息。
在这个例子中,backup保留的是没有迭代之前(比如站在3的视角,它不会知道1→2的距离,即使1→2的距离在2→3之前更新,这样就不会因为1→2dist的确定,而串联确定2→3)
算法实现:
1. 初始化源点s到各个点v的路径dis[v] = ∞,dis[s] = 0。
2. 进行n - 1次遍历,每次遍历对所有边进行松弛操作,满足则将权值更新。
松弛操作:以a为起点,b为终点,ab边长度为w为例。dis[a]代表源点s到a点的路径长度,dis[b]代表源点s到b点的路径长度。如果满足下面的式子则将dis[b]更新为dis[a] + w。
dis[b] > dis[a] + w
3. 遍历都结束后,若再进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路。
代码实现
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge // 边,a表示出点,b表示入点,w表示边的权重
{
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > dist[a] + w)
dist[b] = dist[a] + w;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
- 在上面代码中,是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
Bellman-ford算法,直接就是两层循环,内层循环所有边,外层循环就是循环所有边的次数,这个外层循环次数k一般是题目控制的,也即经过不超过k条边走到n号点。时间复杂度是O(n*m)
Bellman—Ford算法的队列优化还有一个名字叫做SPFA。
优化原理
简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。用数组dis记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
#include <iostream>
#include <algorithm>
#include <string.h>
#include <queue>
#define inf 1e5+5
using namespace std;
int n,m,s,t;
int vis[1005],cost[1005],h[205];
int tot;
struct Node //结点信息
{
int to,w,next;
}road[3000];
struct Node2
{
int cost1,to;
friend bool operator < (Node2 x,Node2 y)
{
return x.cost1 > y.cost1;
}
}road1[3000];
void add(int u,int v,int w) //链式前向星存储邻接表
{
road[tot].to = v;
road[tot].w = w;
road[tot].next = h[u];
h[u] = tot++;
}
void SPFA(int s)
{
priority_queue <Node2> pq;
int e = 0;
memset(vis,0,sizeof(vis));
cost[s] = 0;
road1[e].cost1 = 0;
road1[e].to = s;
pq.push(road1[e]);
while(!pq.empty())
{
int x = pq.top().to;
pq.pop();
if(vis[x] ++) continue;
for(int i = h[x]; i != -1; i = road[i].next)
{
int to = road[i].to;
if(!vis[to] && cost[x] + road[i].w <= cost[to])
{
cost[to] = cost[x] + road[i].w;
e++;
road1[e].cost1 = cost[to];
road1[e].to = to;
pq.push(road1[e]);
}
}
}
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
int u,v,w;
tot = 0;
memset(h,-1,sizeof(h));
for(int x = 0; x < 1005; x++)
{
cost[x] = inf;
}
for(int i = 0; i < m; i++)
{
cin >> u >> v >> w;
add(u,v,w);
add(v,u,w);
}
cin >> s >> t;
SPFA(s);
if(cost[t] == inf)
{
cout << -1 << endl;
}
else
{
cout << cost[t] << endl;
}
}
}
2.4 各顶点间的最短路径问题——Floyd算法
2.4.1 Floyd算法基本思想:
求出每⼀对顶点之间的最短路径,使⽤动态规划思想,将问题的求解分为多个阶段。
2.4.2 Floyd算法应用范围
可以⽤于负权值带权图,但是不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。
2.4.3 算法实现:
-
Floyd算法使用到两个矩阵:
dist[][]
:目前各顶点间的最短路径。path[][]
:两个顶点之间的中转点。
递推一个n阶方阵序列,
,...,
,...,
,其中
[i][j]表示从顶点vi到vj的长度,k表示绕行第k个顶点的运算步骤,利用
记录节点的中转情况。
步骤:①初始时若v0到vi之间有边,则记录其最短路径为该边权值,若不存在则记∞
②尝试允许经过v0顶点中转,更新顶点间最短路径
③依此尝试允许经过v1,v2,...,vk顶点中转,并不断更新最短路径,,直到允许v(n-1)顶点都经过中转,方阵 [i][j] = Min{[i][j] , [i][k]+[k][j]}
④经过n次迭代,最终[i][j]就是vi到vj的最短路径长度
代码实现:
//初始化矩阵A和path
...
for(int k=0; k<n; k++)
{
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
{
if(A[i][j]>A[i][k]+A[k][j])
{
A[i][j] = A[i][k]+A[k][j]; //更新最短路径长度
path[i][j] = k; //中转点
}
}
}
}
算法分析:时间复杂度——O(),空间复杂度——O(
)
3. 有向⽆环图描述表达式
有向⽆环图:若⼀个有向图中不存在环,则称为有向⽆环图,简称DAG图(Directed Acyclic Graph)
DAG描述表达式:((a+b)*(b*(c+d))+(c+d)*e)*((c+d)*e)
求最小顶点数时, 顶点中不可能出现重复的操作数
最少顶点有向无环图描述表达式的解题步骤:
- Step 1:把各个操作数不重复地排成一排
- Step 2:标出各个运算符的生效顺序 (先后顺序有点出入无所谓)
- Step 3:按顺序加入运算符,注意“分层”
- Step 4:从底向上逐层检查同层的运算符是否可以合体
4. 拓扑排序
4.1 AOV网(Activity on Vertex Network,用顶点表示活动的网):
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行。
4.2 拓扑排序定义:
在图论中,由⼀个有向⽆环图的顶点组成的序列,当且仅当满⾜下列条件时,称为该图的⼀个拓扑排序:
- 每个顶点出现且只出现⼀次;
- 若顶点 A 在序列中排在顶点 B 的前⾯,则在图中不存在从顶点 B 到顶点 A 的路径。
或定义为:拓扑排序是对有向⽆环图的顶点的⼀种排序,它使得若存在⼀条从顶点 A 到顶点 B 的路径,则在排序中顶点 B 出现在顶点 A 的后⾯。每个 AOV ⽹都有⼀个或多个拓扑排序序列。
拓扑排序:找到做事的先后顺序
4.3 拓扑排序的实现:
4.3.1 实现流程
① 从AOV⽹中选择⼀个没有前驱(⼊度为0)的顶点并输出。
② 从⽹中删除该顶点和所有以它为起点的有向边。
③ 重复①和②直到当前的AOV⽹为空或当前⽹中不存在⽆前驱的顶点为⽌(说明有回路)。
代码实现拓扑排序(邻接表实现):
#define MaxVertexNum 100 //图中顶点数目最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点位置
struct ArcNode *nextarc; //指向下一条弧的指针
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
// 对图G进行拓扑排序
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)){ //栈不空,则存入
Pop(S,i); //栈顶元素出栈
print[count++]=i; //输出顶点i
for(p=G.vertices[i].firstarc;p;p=p=->nextarc){
//将所有i指向的顶点的入度减1,并将入度为0的顶点压入栈
v=p->adjvex;
if(!(--indegree[v]))
Push(S,v); //入度为0,则入栈
}
}
if(count<G.vexnum)
return false; //排序失败
else
return true; //排序成功
}
4.3.3 性能分析
每个顶点都需要 处理⼀次,每条边都需要处 理⼀次
时间复杂度:O(|V|+|E|) 若采⽤邻接矩阵,则需O(|V|2)
4.4 逆拓扑排序
4.4.1 实现流程
对⼀个AOV⽹,如果采⽤下列步骤进⾏排序,则称之为逆拓扑排序:
① 从AOV⽹中选择⼀个没有后继(出度为0)的顶点并输出。
② 从⽹中删除该顶点和所有以它为终点的有向边。
③ 重复①和②直到当前的AOV⽹为空。
4.4.2 模仿拓扑排序的思想借助逆邻接表实现逆拓扑排序
#define MaxVertexNum 100 //图中顶点数目最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点位置
struct ArcNode *nextarc; //指向下一条弧的指针
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
// 对图G进行逆拓扑排序
bool ReverseTopologicalSort(Graph G){
InitStack(S); //初始化栈,存储出度为0的顶点
for(int i=0;i<g.vexnum;i++){
if(outdegree[i]==0)
Push(S,i); //将所有出度为0的顶点进栈
}
int count=0; //计数,记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不空,则存入
Pop(S,i); //栈顶元素出栈
print[count++]=i; //输出顶点i
for(p=G.vertices[i].firstarc;p;p=p=->nextarc){
//将所有指向i的顶点的出度减1,并将出度为0的顶点压入栈
v=p->adjvex;
if(!(--outdegree[v]))
Push(S,v); //出度为0,则入栈
}
}
if(count<G.vexnum)
return false; //排序失败
else
return true; //排序成功
}
4.4.3 逆拓扑排序的实现(DFS算法),在顶点退栈前输出
5. 关键路径
5.1 AOE 网:
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如 完成活动所需的时间),称之为⽤边表示活动的⽹络,简称 AOE ⽹ (Activity On Edge NetWork)。
AOE⽹具有以下两个性质:
- 只有在某顶点所代表的事件发⽣后,从该顶点出发的各有向边所代表的活动才能开始;
- 只有在进⼊某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发⽣。 另外,有些活动是可以并⾏进⾏的。
源点和汇点:在 AOE ⽹中仅有⼀个⼊度为 0 的顶点,称为开始顶点(源点),它表示整个⼯程的开始; 也仅有⼀个出度为 0 的顶点,称为结束顶点(汇点),它表示整个⼯程的结束。
关键路径和关键活动:
- 从源点到汇点的有向路径可能有多条,所有路径中,具有最⼤路径⻓度的路径称为关键路径,⽽把关键路径上的活动称为关键活动。
- 完成整个⼯程的最短时间就是关键路径的⻓度,若关键活动不能按时完成,则整个 ⼯程的完成时间就会延⻓。
活动和事件的最早和最迟发生时间:
- 事件vk的最早发⽣时间ve(k)——决定了所有从vk开始的活动能够开⼯的最早时间
- 活动ai的最早开始时间e(i)——指该活动弧的起点所表⽰的事件的最早发⽣时间
- 事件vk的最迟发⽣时间vl(k)——它是指在不推迟整个⼯程完成的前提下,该事件最迟必须发⽣的时间
- 活动ai的最迟开始时间l(i)——它是指该活动弧的终点所表示事件的最迟发⽣时间与该活动所需时间之差
- 活动ai的时间余量d(i)=l(i)-e(i),表⽰在不增加完成整个⼯程所需总时间的情况下,活动ai可以拖延的时间
若⼀个活动的时间余量为零,则说明该活动必须要如期完成,d(i)=0即l(i) = e(i)的活动ai是关键活动 由关键活动组成的路径就是关键路径
5.2 求关键路径的步骤
1.求所有事件的最早发生时间ve(): 按拓扑排序序列,依次求各个顶点的 ve(k)即事件的关键路径。
2.求所有事件的最迟发生时间 vl(): 按逆拓扑排序序列,依次求各个顶点的 vl(k)即从后往前推(最后一个点的最早和最迟相等)。
3.求所有活动的最早发生时间 e(): 若边 < vk,v; > 表示活动 ai,则有 e(i) = ve(k)。
4.求所有活动的最迟发生时间 l(): 若边 < Vk,v; > 表示活动 a,则有 l(i) = vl(j)- Weight(vk,v;).
5.求所有活动的时间余量 d(): d() =l(i) - e(i)=最早-最迟。
d(i)=0的活动就是关键活动, 由 关键活动可得关键路径
5.3 关键活动、关键路径的特性:
- 若关键活动耗时增加,则整个⼯程的⼯期将增⻓。
- 缩短关键活动的时间,可以缩短整个⼯程的⼯期。
- 当缩短到⼀定程度时,关键活动可能会变成⾮关键活动。
- 可能有多条关键路径,只提⾼⼀条关键路径上的关键活动速度并不能缩短整个⼯程的⼯期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短⼯期的⽬的。