关键路径算法

概述

1956年,美国杜邦公司提出关键路径法,并于1957年首先用于1000万美元化工厂建设,工期比原计划缩短了4个月。杜邦公司在采用关键路径法的一年中,节省了100万美元。

在项目管理中,关键路径是指网络终端元素的元素的序列,该序列具有最长的总工期并决定了整个项目的最短完成时间。关键路径的工期决定了整个项目的工期。任何关键路径上的终端元素的延迟将直接影响项目的预期完成时间(例如在关键路径上没有浮动时间)。

AOE网

AOE网Activity On Edge)即边表示活动的网,是与AOV网(顶点表示活动)相对应的一个概念。而拓扑排序恰恰就是在AOV网上进行的,这是拓扑排序与关键路径最直观的联系。AOE网是一个带权的有向无环图,其中顶点表示事件(Event),弧表示活动,权表示活动持续的时间。下面的就是一个AOV网:

1647869902616

其中 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网中入度为零的点称为源点,将出度为零的点称为汇点。

1647870405960

关键路径

由于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

1647870657383

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;
}
  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

h0l10w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值