Skip to content

关键路径(AOE 网)

为什么需要关键路径

在工程项目管理中,一个大工程往往可以分解为若干子任务(活动),这些活动之间存在先后依赖关系。关键路径就是整个工程中耗时最长的一条路径,它决定了工程的最短完工时间。

408 考研中,关键路径属于图的应用章节,通常以选择题或应用题的形式出现,要求手动计算各事件/活动的时间参数并找出关键路径。

核心思想

关键路径的核心要素:

  • AOE 网(Activity On Edge):用有向无环图表示工程,顶点表示事件(Event),有向边表示活动(Activity),边的权值表示活动持续时间
  • 关键路径 = 从源点到汇点的最长路径:路径长度等于各边权值之和,最长路径决定工程最短工期
  • 关键活动:最早开始时间 e(i) 等于最晚开始时间 l(i) 的活动,即时间余量为 0 的活动

求解思路:先用拓扑排序求事件的最早发生时间 ve[],再用逆拓扑排序求事件的最晚发生时间 vl[],最后由 ve[] 和 vl[] 推导每条活动的 e[] 和 l[]。

交互可视化

通过下方的交互动画,你可以逐步观察关键路径的执行过程:

加载可视化中...

操作详解

基本概念

四个核心时间参数:

符号含义计算方式
ve(j)事件 j 的最早发生时间ve(j) = max{ ve(i) + w(i,j) },取所有前驱的最大值
vl(j)事件 j 的最晚发生时间vl(j) = min{ vl(k) - w(j,k) },取所有后继的最小值
e(i)活动 aᵢ 的最早开始时间e(i) = ve(该活动的起点)
l(i)活动 aᵢ 的最晚开始时间l(i) = vl(该活动的终点) - w(aᵢ)

关键概念辨析:

  • 事件(顶点):表示某些活动已完成、另一些活动可以开始的状态
  • 活动(边):表示实际需要耗费时间的子任务
  • 源点:入度为 0 的顶点(工程开始),ve(源点) = 0
  • 汇点:出度为 0 的顶点(工程结束),vl(汇点) = ve(汇点)
  • 关键活动:满足 e(i) == l(i) 的活动,即 l(i) - e(i) == 0

求关键路径过程

关键步骤:

  1. 对 AOE 网进行拓扑排序,同时按拓扑序计算每个事件的 ve[]
  2. 逆拓扑序计算每个事件的 vl[](初始化 vl[汇点] = ve[汇点])
  3. 遍历每条活动边,计算 e[] 和 l[]
  4. 找出所有 e(i) == l(i) 的活动,即为关键活动,关键活动构成关键路径
cpp
#include <cstring>
#include <stack>
#include <queue>
using namespace std;

const int MAXV = 1005;

struct Edge {
    int to, w, next;
} edges[MAXV * MAXV];

int head[MAXV], cnt;
int inDeg[MAXV];
int ve[MAXV], vl[MAXV];  // 事件最早/最晚发生时间
int n, m;                 // n个事件, m个活动

void addEdge(int u, int v, int w) {
    edges[cnt] = {v, w, head[u]};
    head[u] = cnt++;
    inDeg[v]++;
}

// 拓扑排序 + 求 ve[]
bool topoSort(stack<int>& topoStack) {
    queue<int> q;
    for (int i = 0; i < n; i++)
        if (inDeg[i] == 0) q.push(i);

    int count = 0;
    memset(ve, 0, sizeof(ve));

    while (!q.empty()) {
        int u = q.front(); q.pop();
        topoStack.push(u);  // 记录拓扑序,用于逆序求 vl
        count++;
        for (int i = head[u]; i != -1; i = edges[i].next) {
            int v = edges[i].to, w = edges[i].w;
            if (ve[u] + w > ve[v])
                ve[v] = ve[u] + w;  // 取最大值
            if (--inDeg[v] == 0)
                q.push(v);
        }
    }
    return count == n;  // 判断是否有环
}

// 求关键路径
void criticalPath() {
    stack<int> topoStack;
    if (!topoSort(topoStack)) return;  // 有环则无关键路径

    // 初始化 vl[] 为汇点的 ve 值
    int maxVe = 0;
    for (int i = 0; i < n; i++)
        if (ve[i] > maxVe) maxVe = ve[i];
    for (int i = 0; i < n; i++)
        vl[i] = maxVe;

    // 逆拓扑序求 vl[]
    while (!topoStack.empty()) {
        int u = topoStack.top(); topoStack.pop();
        for (int i = head[u]; i != -1; i = edges[i].next) {
            int v = edges[i].to, w = edges[i].w;
            if (vl[v] - w < vl[u])
                vl[u] = vl[v] - w;  // 取最小值
        }
    }

    // 遍历每条活动,判断是否为关键活动
    for (int u = 0; u < n; u++) {
        for (int i = head[u]; i != -1; i = edges[i].next) {
            int v = edges[i].to, w = edges[i].w;
            int e = ve[u];        // 活动最早开始时间
            int l = vl[v] - w;    // 活动最晚开始时间
            if (e == l) {
                // <u, v> 是关键活动
            }
        }
    }
}

时间余量分析

活动 aᵢ 的时间余量 d(i) = l(i) - e(i),表示该活动在不影响工期的前提下可以推迟的最大时间。

  • d(i) == 0:关键活动,不能推迟,否则整个工期延长
  • d(i) > 0:非关键活动,可以在 d(i) 范围内推迟开始或延长工期

注意事项:

  • 关键路径可能不唯一,所有关键活动构成的路径都是关键路径
  • 缩短工期只能通过缩短关键活动的持续时间来实现
  • 缩短某关键活动后,该路径可能不再是关键路径(其他路径变为最长),需要重新计算
  • 只有缩短所有关键路径上公共的关键活动,才能确保工期缩短

复杂度分析

操作时间复杂度说明
拓扑排序求 ve[]O(V + E)遍历所有顶点和边各一次
逆拓扑序求 vl[]O(V + E)遍历所有顶点和边各一次
求 e[] 和 l[]O(E)遍历所有活动边
总体O(V + E)与拓扑排序同阶

空间复杂度:O(V + E),需要存储图结构、ve[]、vl[] 及辅助栈/队列。

考研高频考点

  • ⭐ 手算 ve[]、vl[]、e[]、l[] 四组时间参数(应用题高频,必须熟练)
  • ⭐ 根据 e(i) == l(i) 判断关键活动、找出关键路径(选择题/应用题必考)
  • ⭐ 关键路径与工程最短工期的关系(工期 = 关键路径长度 = ve[汇点])
  • ⭐ AOE 网 vs AOV 网的区别(AOV 用顶点表示活动,AOE 用边表示活动)
  • 关键路径不唯一时,缩短工期的条件分析(需缩短所有关键路径的公共关键活动)
  • 关键路径算法与拓扑排序的关系(关键路径以拓扑排序为基础)