概述
1956年,美国杜邦公司提出关键路径法,并于1957年首先用于1000万美元化工厂建设,工期比原计划缩短了4个月。杜邦公司在采用关键路径法的一年中,节省了100万美元。
在项目管理中,关键路径是指网络终端元素的元素的序列,该序列具有最长的总工期并决定了整个项目的最短完成时间。关键路径的工期决定了整个项目的工期。任何关键路径上的终端元素的延迟将直接影响项目的预期完成时间(例如在关键路径上没有浮动时间)。
AOE网
AOE网(Activity On Edge)即边表示活动的网,是与AOV网(顶点表示活动)相对应的一个概念。而拓扑排序恰恰就是在AOV网上进行的,这是拓扑排序与关键路径最直观的联系。AOE网是一个带权的有向无环图,其中顶点表示事件(Event),弧表示活动,权表示活动持续的时间。下面的就是一个AOV网:
其中 V 1 , … , V 9 V_{1},…,V_{9} V1,…,V9表示事件, a 1 , a 2 , … , a 11 a_{1},a_{2},…,a_{11} a1,a2,…,a11 表示活动,活动的取值表示完成该活动所需要的时间,如 a 1 = 6 a_{1}=6 a1=6表示完成活动 a 1 a_{1} a1所需要的时间为6天。此外,每一事件 V i V_{i} Vi表示在它之前的活动已经完成,在它之后的活动可以开始,如 V 5 V_{5} V5表示活动 a 4 a_{4} a4和 a 5 a_{5} a5已经完成,活动 a 7 a_{7} a7和 a 8 a_{8} a8可以开始了。
AOE网的源点和汇点
由于一个工程中只有一个开始点和一个完成点,故将AOE网中入度为零的点称为源点,将出度为零的点称为汇点。
关键路径
由于AOE网中的有些活动是可以并行进行的(如活动 a 1 a_{1} a1、 a 2 a_{2} a2和 a 3 a_{3} a3就是可以并行进行的),所以完成工程的最短时间是从源点到汇点的最长路径的长度。路径长度最长的路径就叫做关键路径(Critical Path)。如下图中红色顶点和有向边构成的就是一条关键路径,关键路径的长度就是完成活动 a 1 a_{1} a1、 a 4 a_{4} a4和 a 7 a_{7} a7、 a 10 a_{10} a10所需要的时间总和,即为 6+1+9+2 = 18
ETV
ETV(Earliest Time Of Vertex):事件最早发生时间,就是顶点的最早发生时间。
事件 V 2 V_{2} V2的最早发生时间表示从源点 V 1 V_{1} V1出发到达顶点 V 2 V_{2} V2经过的路径上的权值之和,从源点 V 1 V_{1} V1出发到达顶点 V 2 V_{2} V2只经过了权值为6的边,则 V 2 V_{2} V2的最早发生时间为6,表示在活动 a 1 a_{1} a1完成之后,事件 V 2 V_{2} V2才可以开始;同理,事件 V 6 V_{6} V6要发生(即最早发生)需要活动 a 3 a_{3} a3和活动 a 6 a_{6} a6完成之后才可以,故事件 V 6 V_{6} V6的最早发生时间为 5 + 2 = 7。其他顶点(事件)的最早发生时间同理可的。需要说明,事件的最早发生时间一定是从源点到该顶点进行计算的.
LTV
LTV(Latest Time Of Vertex):事件最晚发生时间,就是每个顶点对应的事件最晚需要开始的时间,如果超出此时间将会延误整个工期。
上图中的关键路径( V 1 V_{1} V1, V 2 V_{2} V2 , V 5 V_{5} V5 , V 7 V_{7} V7 , V 9 V_{9} V9 )的长度为18,为什么要提这个长度呢,因为要计算某一个事件的最晚发生时间,我们需要从汇点进行倒推。计算顶点 V 2 V_{2} V2的最晚发生时间为例,已知关键路径的长度为18,事件 V 2 V_{2} V2到汇点 V 9 V_{9} V9所需要的时间为 1 + 9 + 2 = 12,则事件 V 2 V_{2} V2的最晚发生时间为18-12 = 6.再来计算一下事件 V 6 V_{6} V6的最晚发生时间,事件 V 6 V_{6} V6到汇点 V 9 V_{9} V9所需要的时间为 4 + 4 = 8,则事件 V 6 V_{6} V6的最晚发生时间为 18 - 8 = 10;相当于说活动 a 6 a_{6} a6完成之后,大可以休息 3天,再去完成活动 a 9 a_{9} a9也不会影响整个工期。
ETE
ETE(Earliest Time Of Edge):活动的最早开工时间,就是弧的最早发生时间。
活动 a 4 a_{4} a4要最早开工时间为事件 V 2 V_{2} V2的最早发生时间 6;同理,活动 a 9 a_{9} a9的最早发生时间为事件 V 6 V_{6} V6的最早发生时间 7。显然活动的最早开工时间就是活动发生前的事件的最早开始时间。
LTE
LTE(Lastest Time of Edge):活动的最晚发生时间,就是不推迟工期的最晚开工时间。
活动的最晚发生时间则是基于事件的最晚发生时间。比如活动 a 4 a_{4} a4的最晚发生时间为事件 V 5 V_{5} V5的最晚发生时间减去完成活动 a 4 a_{4} a4所需时间,即 7 - 1 = 6;活动 a 9 a_{9} a9的最晚发生时间为事件 V 8 V_{8} V8的最晚发生时间减去完成活动 a 4 a_{4} a4所需时间,即 14 - 4 = 10;
从上面也就可以看出 只要知道了每一个事件(顶点)的ETV 和 LTV,就可以推断出对应的 ETE 和 LTE . 此外还需要注意,关键路径是活动的集合,而不是事件的集合,所以当我们求得 ETV 和 LTV 之后,还需要计算 ETE 和 LTE 。
算法思想
求关键路径的过程事实上最重要的就是上面提到的四个概念,ETV、LTV、ETE 和 LTE,求得了ETE与LTE之后只需要判断两者是否相等,如果相等则为关键路径中的一条边,则输出。
首先通过拓扑排序获得每一个事件的最早发生时间:源点的最早发生时间肯定为0,其他节点的ETV也初始化为0.在拓扑排序的过程中,对于每一条边都有一个起点和终点,起点的ETV加上这条边上的活动的时间如果比终点的ETV大,则更新终点的ETV。
根据事件的最早发生时间ETV推断事件的最晚发生时间 LTV:初始时将LTV的值初始化为汇点的时间,然后根据上面求出的拓扑排序逆序遍历,对于每一条边的起点和终点,如果终点的ETV-活动的时间小于起点的LTV,则更新。
计算活动的最早与最晚发生时间:从源点 V 1 V_{1} V1开始,遍历源点的邻接顶点 V 2 V_{2} V2, 将弧 < V 1 V_{1} V1, V 2 V_{2} V2> 上的活动 a 1 a_{1} a1的最早发生时间更新为源点 V 1 V_{1} V1的最早发生时间 0,将活动 a 1 a_{1} a1的最晚发生时间更新为事件 V 2 V_{2} V2的最晚发生时间 6 减去活动 a 1 a_{1} a1所需要的时间6,即为0. 判断活动 a 1 a_{1} a1的最早发生时间与最晚发生时间是否相等,如果相等则为关键活动,并输出。同理遍历源点 的邻接顶点 V 3 V_{3} V3和 V 4 V_{4} V4,并更新弧 a 2 a_{2} a2和 a 3 a_{3} a3的最晚发生时间与最早发生时间。后续节点的遍历同理。
代码实现
输入格式:
第一行两个整数numVertexes,numEdges分别表示节点数和边数
接下来numEdges行每行三个整数x,y,z,表示从节点x到节点有一条边,该边上的活动所需时间为z
说明:节点编号从1开始
输出格式:
从源点开始每行以<a,b> c格式输出,表示关键路径中的一段从a到b,该段时间为c
样例输入:
9 11
1 2 6
1 3 4
1 4 5
2 5 1
3 5 1
4 6 2
5 7 9
5 8 7
6 8 4
7 9 2
8 9 4
样例输出:
<1,2> 6
<2,5> 1
<5,8> 7
<5,7> 9
<7,9> 2
<8,9> 4
C++实现:
#include <iostream>
#include <stack>
#include <cstring>
#include <vector>
using namespace std;
//边表节点的声明
struct EdgeNode
{
int adjvex; //用于保存该边的终点
int time; //这条边上的活动所需时间
EdgeNode *next;
};
//顶点表节点的声明
struct VertexNode
{
int indegree; //节点入度
int data; //数据
EdgeNode *firstEdge; //从该顶点出去的所有边中的第一条
};
const int MAX = 10001;
int numVertexes, numEdges; //顶点数,边数
int etv[MAX], ltv[MAX]; //事件最早发生时间,事件最晚发生时间
VertexNode graph[MAX]; //用于存图
/**
* @brief 如果拓扑排序存在,返回true并将拓扑保存在topo中,否则返回false
*
* @param topo
* @return true
* @return false
*/
bool TopologicalSort(vector<int> &topo)
{
memset(etv, 0, sizeof(etv));
stack<int> stack;
for (int i = 0; i < numVertexes; i++)
{
if (graph[i].indegree == 0)
stack.push(i); //将入度为0的顶点下标入栈
}
while (!stack.empty())
{
int top = stack.top(); //获取栈顶节点编号
stack.pop(); //出栈
topo.push_back(top); //保存拓扑排序结果
//遍历该节点所有连边
for (EdgeNode *e = graph[top].firstEdge; e; e = e->next)
{
int end = e->adjvex; //该边终点
graph[end].indegree--; //入度减一
if (graph[end].indegree == 0)
stack.push(end);
if (etv[top] + e->time > etv[end]) //更新etv
etv[end] = etv[top] + e->time;
}
}
if (topo.size() < numVertexes) //如果保存的拓扑序列长度小于节点数,说明存在环
return false;
else
return true;
}
void CriticalPath()
{
vector<int> topo;
TopologicalSort(topo);
//初始化ltv都为汇点的时间
for (int i = 1; i <= numVertexes; i++)
ltv[i] = etv[numVertexes];
//从汇点倒过来逐个计算ltv
for (int i = topo.size() - 1; i >= 0; i--)
{
int pre = topo[i];
for (EdgeNode *e = graph[pre].firstEdge; e; e = e->next)
{
int end = e->adjvex; //该边终点
if (ltv[end] - e->time < ltv[pre])
ltv[pre] = ltv[end] - e->time;
}
}
//通过etv和ltv求ete和lte
for (int i = 1; i <= numVertexes; i++)
{
for (EdgeNode *e = graph[i].firstEdge; e; e = e->next)
{
int end = e->adjvex;
int ete = etv[i];
int lte = ltv[end] - e->time;
if (ete == lte)
cout << "<" << i << "," << end << ">"
<< " " << e->time << endl;
}
}
}
int main()
{
cin >> numVertexes >> numEdges;
for (int i = 0; i < numEdges; i++)
{
int x, y, z;
cin >> x >> y >> z;
graph[y].indegree++;
EdgeNode *e = new EdgeNode;
e->adjvex = y;
e->time = z;
e->next = graph[x].firstEdge;
graph[x].firstEdge = e;
}
CriticalPath();
return 0;
}