关键路径

算法理论

AOV网与AOE网

  • AOV网(Activity On Vertex):顶点表示活动,边表示活动间的优先关系的有向无环图;
  • AOE网(Activity On Edge):带权的边表示活动,而用顶点表示事件的有向无环图;边权表示完成活动需要的时间;
  • AOE网中的最长路径被称为关键路径,关键路径上的活动称为关键活动,关键活动会影响整个工程的进度
  • 理解:关键路径的定义是AOE网中的最长路径,为什么其长度会等于整个工程的最短完成时间呢?
    **A:**从时间的角度上看,不能拖延的活动严格按照时间表所达到的就是最短时间;而从路径长度的角度上看,关键路径选择的总是最长的道路。

最长路径

  • 对一个没有正环的图(指从源点可达的正环),如果需要求最长路径长度,则可以把所有边的边权乘以-1,然后使用Bellman-Ford算法或SPFA算法求最短路径,将所得结果取反即可;
  • 如果图中有正环,那么最长路径是不存在;但如果要求最长简单路径(每个顶点最多只经过一次),那么虽然最长简单路径存在,却无法通过Bellman-Ford等算法得到,原因是最长路径问题是NP-Hard问题(即没有多项式时间复杂度算法可解决);
  • 如果求有向无环图的最长路径长度,关键路径的求法比上面取反用SPFA等算法更快;

关键路径

思路:即求解DAG(有向无环图)中最长路径的方法 - 先求点,再夹边

  • 1、按照拓扑序和逆拓扑序分别计算个顶点(事件)的最早发生时间和最迟发生时间:
    • 最早(拓扑序):ve[j] = max { ve[i] + length[i->j] }j的所有入边)
    • 最迟(逆拓扑序):vl[i] = min { vl[j] - length[i->j] }i的所有出边)
  • 2、用上面的计算结果计算各(活动)的最早开始时间和最迟开始时间:
    • 最早e[i->j] = ve[j]
    • 最迟l[i->j] = vl[j] - length[i->j]
  • 3、e[i->j] == l[i->j] 的边(活动)即为关键活动

代码实现

适用于 汇点确定且唯一 的情况,以n-1号顶点为汇点为例;

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
const int maxn = 10010;

struct edge{
    int v; //终点
    int w; //边权
};

vector<edge> Adj[maxn]; //邻接表(边带权值)
int in_degree[maxn]; //入度
int N; // 顶点数
stack<int> topOrder; //栈 保存拓扑序列
int ve[maxn]; //结点的最早发生时间
int vl[maxn]; //结点的最迟发生时间


//拓扑排序 顺便求ve数组
bool topLocgicalSort() {
    queue<int> Q;
    for(int i = 0; i < N; i++) { //所有入度为0的结点入队
        if(in_degree[i] == 0)
            Q.push(i);
    }
    while(!Q.empty()) {
        int u = Q.front();  //取队首结点
        Q.pop();
        topOrder.push(u); //保存拓扑序列
        for(int i = 0; i < Adj[u].size(); i++) {
            int v = Adj[u][i].v; //u的后集结点v
            in_degree[v]--;    //结点v的入度-1
            if(in_degree[v] == 0)  //入度减为0时入队
                Q.push(v);

            //用ve[u]来更新u的所有后继结点
            if(ve[u] + Adj[u][i].w > ve[v]) { //选最大值
                ve[v] = ve[u] + Adj[u][i].w;
            }
        }
    }//while
    if(topOrder.size() == N) return true; //拓扑排序成功
    else return false; //失败
}

//颠倒拓扑序列得到一组合法的逆拓扑序列,求vl数组
void get_vl() {
    while(!topOrder.empty()) {
        int u = topOrder.top();
        topOrder.pop();
        for(int i = 0; i < Adj[u].size(); i++) {
            int v = Adj[u][i].v;
            //用u的所有后继结点v的vl来更新vl[u]
            if(vl[v] - Adj[u][i].w < vl[u]) { //选最小值
                vl[u] = vl[v] - Adj[u][i].w;
            }
        }//for-i
    }//while
}//get_vl

//关键路径
//使用动态规划可以更简洁地求解关键路径(11.6节)
int CriticalPath() {
    memset(ve, 0, sizeof(ve)); //ve数组初始化为0
    if(topLocgicalSort() == false) { //计算ve[]值
        return -1; //非有向无环图
    }
    fill(vl, vl + maxn, ve[N - 1]); //初始化vl数组,值为终点(汇点)的ve值    

    get_vl(); //计算vl[]值

    //遍历邻接表的所有边(代表活动),计算活动的最早开始时间e和最迟开始是按l
    for(int u = 0; u < N; u++) {
        for(int i = 0; i < Adj[u].size(); i++) {
            int v = Adj[u][i].v, w = Adj[u][i].w;
            //计算活动(边)的最早开始时间e和最迟开始时间l
            int e = ve[u], l = vl[v] - w;
            if(e == l) { //若e==l,则u->v为关键活动
                printf("%d->%d\n", u, v);
                //如果需要完整输出关键路径,保存即可(建一个邻接表,保存u->v)
            }
        }//for - i
    }//for - u

    return ve[N -1]; //返回关键路径长度
}//CriticalPath

1、如果实现不知道汇点编号,如何比较快地获取关键路径长度?
ve[]数组中的最大值为汇点即可;
原因:ve[]数组的含义是时间的最早开始时间,因此所有事件中最大的一定是最后一个(或多个)时间,即汇点;
代码操作:fill()函数之前添加一段语句,改变vl[]函数初始值即可;

int maxLength = 0;
for (int i = 0; i < n; i++) { //找到ve[]中的最大值
	if (ve[i] > maxLength) maxLength = ve[i];
}
fill (vl, vl + n, maxLength);
  • 2、如果想输出完整路径,就需要把关键活动存下来;
    方法是新建一个邻接表,当确定u->v是关键活动时,将其加入邻接表,这样最后生成的就是所有关键路径合成的,最后可以用DFS遍历来获取所有关键路径。(即可能有多条路径)
  • 3、使用动态规划的做法可以更简洁地求解关键路径,补充完成后会加入链接!!

应用练习

王道P218(清华大学)

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <map>
#include <stack>
#include <queue>
using namespace std;
const int maxn = 505;
int n, m;
int INF = 1e8;
int inDegrees[maxn];
vector<int> Adj[maxn];
int weight[maxn];
stack<int> topSortStack;
int fi[maxn];
int gi[maxn];

void topSort() {
    queue<int> pq;
    for(int i = 1; i <= n; i++){
        if(inDegrees[i] == 0){
            pq.push(i);
        }
    }
    while(!pq.empty()){
        int top = pq.front();
        topSortStack.push(top);
        pq.pop();
        for(int i = 0; i < Adj[top].size(); i++){
            int v = Adj[top][i];
            inDegrees[v]--;
            fi[v] = max(fi[v], fi[top] + weight[top]);
            if(inDegrees[v] == 0){
                pq.push(v);
            }
        }
    }
}

int main() {
    cin >> n >> m;
    fill(fi, fi+maxn, 0);
    fill(inDegrees, inDegrees + maxn, 0);
    for(int i = 1; i <= n; i++){
        cin >> weight[i];
    }
    int a, b;
    for(int i = 0; i < m; i++){
        cin >> a >> b;
        inDegrees[b]++;
        Adj[a].push_back(b);
    }
    topSort();
    int max = -1;
    for(int i = 1; i <= n; i++){
        if(fi[i] + weight[i]> max){
            max = fi[i] + weight[i];
        }
    }
    while(!topSortStack.empty()){
        int top = topSortStack.top();
        topSortStack.pop();
        if(Adj[top].size() == 0){
            gi[top] = max - weight[top];
        }else{
            gi[top] = INF;
        }
        for(int i = 0; i < Adj[top].size(); i++){
            int v = Adj[top][i];
            gi[top] = min(gi[top], gi[v] - weight[top]);
        }
    }
    max = -1;
    for(int i = 1; i <= n; i++){
        if(gi[i] + weight[i] > max){
            max = gi[i] + weight[i];
        }
    }
    long long result = 1;
    long long constant = 1000000000 + 7;
    for(int i = 1; i <= n; i++){
        result = (result * (gi[i] - fi[i] + 1)) % (constant);
    }
    cout << max << endl;
    cout << result << endl;
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值