<数据结构>关键路径
AOV网和AOE网
AOV网
顶点活动网络(Activity On Vertex, AOV):用顶点表示活动,边集表示活动优先关系的有向图。
上图中,结点表示课程,有向边表示课程的先导关系。
显然,图中不应该出现有向环,否则会让优先关系出现逻辑错误。
AOE网
定义
边活动网络(Acitivity On Edge, AOE):用带权的边集表示活动,用顶点表示事件的有向图。AOE比AOV包含更多信息。
上图中,a1-a6表示活动,即要学习的课程;边权表示学习时间;结点表示事件,如v2表示空间解析几何已经学完,可以开始学习复变函数,v5表示泛函分析的先导课程已经学完,接下来可以开始学习泛函分析。
所以: “事件” 仅表示一个 中介状态 。
显然,图中不应该出现有向环,否则会让优先关系出现逻辑错误。
与AOV网的转化
- AOV网中的顶点:拆分为两个顶点,分别表示活动的起点和终点(在AOE网中就是两个事件),而两个顶点间用有向边连接,该有向边就表示AOV网中原顶点的活动,最后在赋予边权。
- AOV网中的边:原AOV网中的边全部视为空活动,边权为0。
AOE网中着重解决的两个问题
AOE网是基于工程提出的概念,它着重解决两个问题
1.最长路径问题
工程起始到终止至少需要多少时间 取决于AOE网中的最长路径。
如何理解此处的“至少”和“最长”? 设想你有一排手办,而你要定制一个展示盒把所有的手办全部装在里面展示,那么,你所有定制的展示盒的最低高度显然就取决于你那一排手办里最高高度的手办。也可以从反面理解,木桶平放时所能装下的最高水位取决于木桶的最低板长。
2.关键活动问题
哪条(些)路径上的活动是影响整个工程进度的关键:显然最长路径上的活动是影响整个工程进度的关键,如果缩短了最长路径上活动的时间,就能缩短工程的总体时间,反之,就会延长。比如,如果一排手办中最高的手办的高度由1m变成0.5m,那么所要定制的展示盒就只需要0.5m高而不在是1m高。
总结
所以:我们称最长路径为“关键路径”,称最长路径上的活动为“关键活动”。它们是影响工程时间的关键。
如果我们求出了关键路径,就能求出工程最短时间。
而需要求出工程最短时间,必然要借助关键路径。
最长路径
无正环的图
如果一个图中没有正环(指从原点可达的正环),那么只需要把边权乘以-1,令其变成相反数,就可以将最长路径问题转化为最短路径问题,使用Bellman-Ford或SPFA解决,注意不能使用Dijstra(无法处理负权边)。
最短路径问题介绍:<数据结构>图的最短路径问题
有向无环图的最短路径
见下文“关键路径算法”
其他情况
为NP-Hard问题(无法用 多项式时间复杂度的算法解决 的问题)。
关键路径算法:确定关键活动,求出工程最短时间
前置定义
e[a]: 活动a的最早发生时间
l[a]: 活动a的最晚发生时间
若 "e[a] == l[a]" 说明活动a不能拖延,活动a是关键活动。
ve[i]: 事件i的最早发生时间
le[i]: 事件i的最晚发生时间
求解e[a]、l[a]转化为求解ve[i]、le[i]这两个新数组
- 对于活动ar来说,只要在事件Vi发生是马上开始,就可以使得活动ar开始的时间最早,因此e[r] = ve[i]
- 如果l[r]是活动ar的最迟发生时间,那么l[r] + length[r]就是事件Vj的最迟发生时间(length[r]表示活动a的边权,即活动a持续时间)。因此l[r] = vl[j] - length[r]。
下面讨论如何求解ve[ ]与vl[ ]
ve数组求解
数学分析
有k个事件 Vi1 ~ Vik,通过相应的活动ar1 ~ ark, 到达事件Vj。(如下图)
活动的边权分别为length[r1] ~ length[rk]。
假设 Vi1 ~ Vik 时间的最早发生时间 ve[i1] ~ ve[ik] 以及 length[r1] ~ length[rk] 均已知, 则 事件Vj发生的最早时间就是ve[i1]+length[r1] ~ ve[ik]+length[rk] 中的 最大值
此处时间节点Ve[]取最大值 是因为只有取最大值才能保证,在Vj开始时,Vj的所有先导事件都已完成。
代码实现:拓扑排序
拓扑排序介绍:<数据结构>拓扑排序
-
根据上文分析,要想知道ve[j],那么ve[i1]~ve[ik]必须得到。 即在访问某个结点时保证它的前驱结点都已经被访问过 ————> 拓扑排序
-
在拓扑排序中无法根据当前结点得到它的前驱结点————>访问到某个结点Vi时,不去寻找它的前驱结点,而是用它更新所有后继结点的ve[]值,这样,在访问它的后继结点时,后继结点的ve值必然是已经被更新过的。
stack<int> topOrder; //拓扑序列,为后面的逆拓扑排序做准备
//拓扑排序,顺便求ve数组。 ve数组初始化为0
bool topologicalSort(){
queue<int> q;
for(int i = 0; i<n; i++){
if(inDegree[i] == 0){
q.push(i);
}
}
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u); //将u加入拓扑序列
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的i号后继结点的编号为v
inDegree[v]--;
if(inDegree[v] == 0){
q.push(v);
}
//用u来更新u的所有后继结点v
if(ve[u]+G[u][i].w > ve[v]){
ve[v] = ve[u] + G[u][i].w;
}
}
}
if(topOrder.size() == n) return true;
else return false;
}
vl数组求解
数学分析
从事件Vi出发,通过相应的活动 ar1 ~ ark 可以到达k个事件 Vj1 ~ Vjk ,活动的边权为 length[rl] ~ length[rk]。
假设 Vj1 ~ Vjk 时间的最迟发生时间 vl[j1] ~ vl[jk] 以及 length[r1] ~ length[rk] 均已知, 则 事件Vi发生的最迟时间就是vl[j1]-length[r1] ~ vl[jk]-length[rk] 中的 最小值
此处时间节点Vl[ik]取最小值 是因为只有取最小值才能保证,在Vi开始时,Vi的所有后继事件都能(在其最晚时间节点前)完成。
代码实现:逆拓扑排序
算出vl[i]需保证i的后继结点的最迟时间即 vl[j1] ~ vl[jk] 都已被算出。
与求ve数组的过程反向,即将拓扑排序序列逆序访问,同时更新vl值即可。
fill(vl,vl+n, ve[n-1]); //vl数组初始化,初始值为汇点的ve值
//直接使用topOrder出栈即为逆拓扑序列,求解vl数组
while(!topOrder.empty()){
int u = topOrder.top(); //栈顶元素为u
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的后继结点v
//用u的所有后继结点v的vl值来更新vl[u]
if(vl[v] - G[u][i].w < vl[u]){
vl[u] = vl[u] - G[u][i].w;
}
}
}
关键路径算法实现
基本步骤
先求点,再夹边:按下面三个步骤进行
主体代码
适用于汇点唯一且确定 的情况,以n-1号顶点为汇点为例。
#include<stdio.h>
#include<vector>
#include<queue>
#include<stack>
#include<algorithm>
using namespace std;
const int MAXV = 100;
struct Node{
int v, w;
};
int ve[MAXV];
int vl[MAXV];
int n; //顶点数
int inDegree[MAXV]; //储存结点入度,在主函数中初始化
vector<Node> G[MAXV]; //邻接表表示图G
stack<int> topOrder; //拓扑序列,为后面的逆拓扑排序做准备
bool topologicalSort(); //拓扑排序,计算ve数组(对应步骤1)
int CirticalPath(); //逆拓扑排序 与 输出关键活动和换件路径长度(对应步骤2、3)
//关键路径,不是有向无环图返回-1,否则返回关键路径长度
int CirticalPath(){
fill(ve, ve+n, 0); //ve数组初始化为0,则汇点的ve值(0+关键路径长度)就等于关键路径长度。
if(topologicalSort() == false){ //调用拓扑排序函数,计算ve数组
return -1; //不是有向无环图,返回-1
}
//此时的ve数组以经过拓扑排序已更新赋值,ve[n-1]为拓扑排序终点(即有向图汇点)的ve值
fill(vl,vl+n, ve[n-1]); //vl数组初始化,初始值为汇点的ve值
//直接使用topOrder出栈即为 逆拓扑序列 ,求解vl数组
while(!topOrder.empty()){
int u = topOrder.top(); //栈顶元素为u
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的后继结点v
//用u的所有后继结点v的vl值来更新vl[u]
if(vl[v] - G[u][i].w < vl[u]){
vl[u] = vl[u] - G[u][i].w;
}
}
}
//遍历邻接表的所有边,计算活动的最早开始时间e和最迟开始时间l
for(int u = 0; u < n; u++){
for(int i = 0; i < G[u].size(); i++){
int v = G[u][v].v, w = G[u][v].w;
//活动的最早开始时间e和最迟开始时间l
int e = ve[u], l = vl[v] - G[u][v].w;
//如果e==l,说明活动u->v是关键路径
if(e == l){
printf("%d->%d\n", u, v);
}
}
}
return ve[n-1]; // 返回关键路径长度
}
//拓扑排序,顺便求ve数组,ve数组初始化为0
bool topologicalSort(){
queue<int> q;
for(int i = 0; i<n; i++){
if(inDegree[i] == 0){
q.push(i);
}
}
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u); //将u加入拓扑序列
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的i号后继结点的编号为v
inDegree[v]--;
if(inDegree[v] == 0){
q.push(v);
}
//用u来更新u的所有后继结点v
if(ve[u]+G[u][i].w > ve[v]){
ve[v] = ve[u] + G[u][i].w;
}
}
}
if(topOrder.size() == n) return true; //无环,可计算关键路径
else return false; //图中有环,无法计算关键路径
}
几个注意点
- 图采用邻接表示实现:<数据结构>图的构建与基本遍历方法
- e和l只是用来判断关键活动,并输出,不必保存。如果需要保存可在结构体Node中添加域e和l。
- 范围拓展:
a.汇点不唯一:引入超级汇点,将所有汇点指向超级汇点,再调用函数CritialPath()
b.汇点不确定:寻找汇点,汇点的ve值必然最大,故可在 fill函数之前找到ve数组最大值即可
替换为:fill(vl, vl+n, v[n-1])
int maxLength = 0; for(int i = 0; i < n; i++){ if(ve[i] > manLength){ maxLength = ve[i]; } }