关键路径(没有正环的图、有向无环图)———附带完整代码以及示例

1 概念

1.1 AOV网、AOE网(图中都不应存有环)

1.1.1 概念

  • 顶点活动(Activity On Vertex,AOV)网:用顶点表示活动,用边集表示活动间优先关系的有向图。
    图1
    图1
  • 边活动(Activity On Edge, AOE)网:用点权边集表示活动,而用顶点表示事件的有向图,其中边权表示完成活动所用的事件。
    • AOE一般用于表示一个工程的进行过程,而工程常常可以分为若干自工程(即活动)
    • 对于工程来说,总会一个起始时刻和结束时刻,因此AOV网一般只有一个源点S(入度为0的点)和一个汇点T(出度为0的点)
    • 如下图所示,新添了四条边a9,a10,a11,a12,边权都为0.在这里插入图片描述

1.1.2 AOV转为AOE

如果给定AOV网中各顶点活动所需的时间,那么就可以把AOV网转为AOE网。
方法为:

  • 1 将AOV网中每个顶点都拆成两个顶点,分别表示活动的起点和终点,而两个顶点之间用有向边连接,该有向边表示原顶点的活动,边权给定;
  • 2 原AOV网中的边全部视为空活动,边权为0;
    图1转为AOE网,得:
    在这里插入图片描述

1.2 关键路径、关键活动

关键路径:AOE网中的最长路径。(工程起始到终止的时间需要多少)
关键活动:关键路径上的活动。(那些路径上的活动是影响整个工程进度的关键)

1.3 最长路径

最长路径(Long Path Problem)问题:寻找图中的最长简单路径。

  • 对于没有正环的图(从源点可达的正环),求最短路径长度:
    • 把所有边的边权乘以-1,令其变为相反数,然后用Bellman-Ford算法或SPFA算法,求最短路径长度,将所得结果取反即可。
  • 如果图中有正环,那么最长路径是不存在的。但是,如果需要求最长简单路径(每个顶点最多只经过一次的路径)是存在的,但却没有办法通过Bellman-Ford等求解,原因是最长路径问题是NP—Hard问题。

2 求解关键路径

由于AOE是有向无环图,而关键路径是图中的最长路径,因此下面给出的是求解有向无环图(DAG)中最长路径的方法。

2.1 准备:

 - e[r]:活动a~r~开始的最早开始时间;
 - l[r]:活动a~r~开始的最迟开始时间;
    +  判断 e[r]是否等与 l[r]来判断是否为关键活动
  - ve[i]:事件i的最早发生事件;
  - vl[i]: 事件i的最迟发生事件;

以下图为例:在这里插入图片描述

  • 1 对于活动ar来说,只要在事件Vj最早发生时马上开始,就可以使得活动ar的开始事件最早,因此e[r] = ve[i];
  • 2 如果l[r]是活动的ar的最迟发生时间,那么l[r] + lengh[r](lengh[r]表示活动ar的边权)就是事件Vj的最迟发生事件,因此l[r] = vl[j] - lenght[r]

因此只需求出数组ve和数组vl,就可以根据上面的公示求出数组e和数组l。

2.2 求数组ve

2.2.1 思路

在这里插入图片描述
如上图所示,事件Vj的最早发生时间为ve[i1] + length[r1] ~ ve[ik]+ lengh[rk]中的最大值(即所有能到达Vj的活动都完成之后,Vj才能被“”激活);

因此如果想获得ve[j]的正确值,ve[i1]~ve[ik]必须已经得到,有什么方法能够在访问某个结点时候,保证它的前驱结点都已经访问完毕?用拓扑排序就可以。

在拓扑排序访问某个结点时,不是让它去找前驱结点来更新ve[i],而是使用ve[i]去更新所有后继结点的ve值。

2.2.2 实现代码

stack<int> topOrder;//拓扑序列


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);
            }
            //用ve[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;
}

2.3 求数组vl

2.3.1 思路

在这里插入图片描述
由上图可知,如果要求出vl[i]的正确值,vl[j1]~vl[jk]都必须已经得。这与求ve数组正好相反,也就是在访问某个结点时保证它的后继结点都已经访问完毕。

颠倒拓扑排序,来得到一组合法的逆拓扑序列。

上面是实现栈来存储拓扑序列,只要要顺序出栈就是逆拓扑序列。

    fill(vl, vl + n, ve[n - 1]);//vl初始化,初值值为汇点ve的值
    //直接用topOrder出栈即逆序拓扑序列,求解vl数组
    while(!topOrder.empty()){
        int u = topOrder.top();

        topOrder.pop();
        for (int i = 0; i < G[u].size(); ++i)
        {
            int v = G[u][i].v;
            if(vl[v] - G[u][i].w < vl[u]){
                vl[u] = vl[v] - G[u][i].w;
            }
        }
    }

2.4 求关键路径

2.4.1 思路( 先求点,再夹边)

  • 1 拓扑序列和逆拓扑序列分别计算各事件的最早、最迟发生时间:
    • 最早(拓扑序列) :
      v e [ j ] = max i , i j v e [ i ] + l e n g h [ i j ] ve[j] = \max_{i,边i到j存在}{ve[i] + lengh[i\rightarrow j]}
    • 最迟(逆拓扑序列:
      v l [ i ] = min j , i j v l [ j ] l e n g h [ i j ] vl[i] = \min_{j,边i到j存在}{vl[j] - lengh[i\rightarrow j]}
  • 2 用上面的结果各边(活动)的最早开始时间和最迟开始时间
    • 最早:
      e [ i j ] = v e [ i ] e[i\rightarrow j] = ve[i]
    • 最迟:
      l [ i j ] = v e [ j ] l e n g t h [ i j ] l[i\rightarrow j] = ve[j] - length[i\rightarrow j]
  • 3 e [ i j ] = l [ i j ] e[i\rightarrow j] = l[i\rightarrow j] 的活动即为关键活动。

2.4.2 实现代码

//关键路径,不是有向无环图返回-1,否则返回关键路径长度
int CriticalPath(){
    memset(ve, 0, sizeof(ve));
    if(topologicalSort() == false){
        return -1;//不是有向无环图返回-1
    }

    fill(vl, vl + n, ve[n - 1]);//vl初始化,初值值为汇点ve的值
    //直接用topOrder出栈即逆序拓扑序列,求解vl数组
    while(!topOrder.empty()){
        int u = topOrder.top();

        topOrder.pop();
        for (int i = 0; i < G[u].size(); ++i)
        {
            int v = G[u][i].v;
            if(vl[v] - G[u][i].w < vl[u]){
                vl[u] = vl[v] - 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][i].v;
            int w = G[u][i].w;
            int e = ve[u];//活动最早开始时间
            int l = vl[u] - w;//或最迟开始时间
            if(e == l){//关键活动
                printf("%d->%d\n", u, v);
            }
        }
    }
    return ve[n -1];//返回关键路径长度
}

2.5 注意

  • 如果实现不知道汇点编号,可以通过取ve数组的最大值。
  • 因为ve数组为事件的最早开始事件,因此所有事件中ve最大的一定是最后一个(或多个)事件,也就是汇点
    实现代码:
    int maxLength = 0;
    for (int i = 0; i < n; ++i)
    {
        if(ve[i] > maxLength){
            maxLength = ve[i];
        }
    }
    fill(vl, vl + n, ve[n - 1]);//vl初始化,初值值为汇点ve的值
  • 如果完整输出所有关键路径,就要把关键活动存储下来。可以新建一个邻接表,当确定u->v是关键活动时,将边u->v加入邻接表。

    3 完整示例

    3.1 题目

问题 A: 关键路径
[命题人 : 外部导入]
时间限制 : 1.000 sec 内存限制 : 128 MB

题目描述
描述:

图的连接边上的数据表示其权值,带权值的图称作网。

上图可描述为顶点集为(a,b,c,d,e)

边集及其权值为(始点,终点 权值):
a b 3
a c 2
b d 5
c d 7
c e 4
d e 6

网的源点是入度为0的顶点,汇点是出度为0的顶点。网的关键路径是指从源点到汇点的所有路径中,具有最大路径长度的路径。上图中的关键路径为a->c->d->e,其权值之和为关键路径的长度为15。

本题的要求是根据给出的网的邻接矩阵求该网的关键路径及其长度。

输入
第一行输入一个正整数n(1<=n<=5),其代表测试数据数目,即图的数目

第二行输入x(1<=x<=15)代表顶点个数,y(1<=y<=19)代表边的条数

第三行给出图中的顶点集,共x个小写字母表示顶点

接下来每行给出一条边的始点和终点及其权值,用空格相隔,每行代表一条边。
输出
第一个输出是图的关键路径(用给出的字母表示顶点, 用括号将边括起来,顶点逗号相隔)

第二个输出是关键路径的长度

每个矩阵对应上面两个输出,两个输出在同一行用空格间隔,每个矩阵的输出占一行。

样例输入 
2
5 6
abcde
a b 3
a c 2
b d 5
c d 7
c e 4
d e 6
4 5
abcd
a b 2
a c 3
a d 4
b d 1
c d 3
样例输出 
(a,c) (c,d) (d,e) 15
(a,c) (c,d) 6

提示
作者:梁青青

3.2 参考代码:

#include <cstdio>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <cstring>
#include <map>

using std::queue;
using std::stack;
using std::vector;
using std::fill;
using std::map;

struct node
{
    int v;
    int w;
    node(int _v, int _w):v(_v), w(_w){}
};

const int MAXN = 20;
vector<node> G[MAXN];//图
int ve[MAXN], vl[MAXN];
int N;
stack<int> topOrder;
int inDegree[MAXN];
map<char, int> letterToIndex;//字母映射数字
vector<int> ans[MAXN];//关键路径
char str[MAXN];//输入的字母串

//拓扑排序
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);
        for (int i = 0; i < G[u].size(); ++i)
        {
            int v = G[u][i].v;
            inDegree[v]--;
            if(inDegree[v] == 0){
                q.push(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;
}


int CriticalPath(){
    memset(ve, 0, sizeof(ve));
    if(topologicalSort() == false){//求ve
        return -1;
    }
    int maxLength = 0;
    for (int i = 0; i < N; ++i)
    {
        if(ve[i] > maxLength){
            maxLength = ve[i];
        }
    }
    fill(vl, vl + MAXN, maxLength);

    while(!topOrder.empty()){//逆拓扑排序,求vl
        int u = topOrder.top();
        topOrder.pop();
        for (int i = 0; i < G[u].size(); ++i)
        {
            int v = G[u][i].v;
            if(vl[v] - G[u][i].w < vl[u]){
                vl[u] = vl[v] - G[u][i].w;
            }
        }
    }

    for (int i = 0; i < N; ++i)//清空上一次关键路径
    {
        ans[i].clear();
    }

    for (int u = 0; u < N; ++u)
    {
        for (int i = 0; i < G[u].size(); ++i)
        {
            int v = G[u][i].v, w = G[u][i].w;
            int e = ve[u], l = vl[v] - w;
            if(e == l){//是关键活动,则存入关键路径
                ans[u].push_back(v);
            }
        }
    }

    int s;
    for (int i = 0; i < N; ++i)//寻找事件起始点
    {
        if(ve[i] == 0){
            s = i;
            break;
        }
    }

    while(ans[s].size()){//没有后继结点时,退出
        printf("(%c,%c) ", str[s], str[ans[s][0]]);
        s = ans[s][0];
    }
    return ve[N - 1];//return maxLength;
}

int main(int argc, char const *argv[])
{
    int n, m, w;
    char a, b;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i)
    {
        memset(inDegree, 0, sizeof(inDegree));

        scanf("%d%d", &N, &m);
        for (int j = 0; j < N; ++j)
        {
            G[j].clear();
        }

        scanf("%s", str);
        for (int j = 0; j < N; ++j)
        {
            letterToIndex[str[j]] = j;
        }

        for (int j = 0; j < m; ++j)
        {
            //getchar();
            scanf("%*c%c %c %d", &a, &b, &w);
            G[letterToIndex[a]].push_back(node(letterToIndex[b], w));
            inDegree[letterToIndex[b]]++;
        }

        int length = CriticalPath();
        printf("%d\n", length);
    }
    return 0;
}
发布了378 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_33375598/article/details/104435237
今日推荐