引言
上一篇我们学习了最小生成树(用最小代价连接所有点)和拓扑排序(给有依赖关系的任务排序)。今天要讲的是图系列的最后一篇------关键路径。
假如你是一个项目经理,要盖一栋楼。有些工序必须按顺序来(先打地基才能砌墙),有些可以同时进行(水电和装修可以并行)。现在老板问你:最快多久能完工?哪些工序一天都不能拖延?
这就是关键路径问题------在一个项目中,找出耗时最长的任务链。这条路径上的任务一旦延期,整个项目就会延期。

第一部分:AOE 网与关键路径概念
一、AOV 网 vs AOE 网
| 网络类型 | 顶点表示 | 边表示 | 用途 |
|---|---|---|---|
| AOV 网(Activity On Vertex) | 活动(任务) | 依赖关系 | 拓扑排序 |
| AOE 网(Activity On Edge) | 事件(状态) | 活动(任务) | 关键路径 |

二、AOE 网的核心术语

源点 :入度为 0 的顶点(项目开始,V1)
汇点:出度为 0 的顶点(项目结束,V5)
四个关键时间量:
| 时间量 | 符号 | 含义 | 计算方式 |
|---|---|---|---|
| 最早发生时间 | ve[j] |
事件 j 最早什么时候能发生 | 正向 递推,取最大值 |
| 最晚发生时间 | vl[j] |
事件 j 最晚什么时候必须发生 | 反向 递推,取最小值 |
| 最早开始时间 | ee[i] |
活动 i 最早什么时候能开始 | ee[i] = ve[起点] |
| 最晚开始时间 | el[i] |
活动 i 最晚什么时候必须开始 | el[i] = vl[终点] - 工期 |
关键活动 :ee[i] == el[i] 的活动,一天都不能拖延 。
关键路径:所有关键活动连成的路径,是项目的最长路径。
第二部分:关键路径算法
一、算法步骤

二、算法过程图解
以盖楼项目为例:

步骤① --- 正向计算 ve(最早发生时间):

步骤② --- 反向计算 vl(最晚发生时间):

步骤③④ --- 计算 ee 和 el,判断关键活动:

第三部分:代码实现
cpp
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <stdbool.h>
#define MAX_V 100
#define INF INT_MAX
typedef struct {
int vertexNum;
int edgeNum;
int matrix[MAX_V][MAX_V]; // 邻接矩阵存边权
} Graph;
void initGraph(Graph* g, int n) {
g->vertexNum = n;
g->edgeNum = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
g->matrix[i][j] = INF; // 无边
}
void addEdge(Graph* g, int u, int v, int w) {
g->matrix[u][v] = w; // 有向边
g->edgeNum++;
}
// 拓扑排序,同时计算 ve
int topologicalOrder(Graph* g, int* ve, int* topo) {
int n = g->vertexNum;
int inDegree[MAX_V] = {0};
int queue[MAX_V], front = 0, rear = 0;
int topoCount = 0;
// 计算入度
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (g->matrix[i][j] != INF)
inDegree[j]++;
// 入度为 0 的入队
for (int i = 0; i < n; i++)
if (inDegree[i] == 0)
queue[rear++] = i;
// 初始化 ve 为 0
for (int i = 0; i < n; i++) ve[i] = 0;
// BFS 拓扑排序 + 计算 ve
while (front < rear) {
int u = queue[front++];
topo[topoCount++] = u;
for (int v = 0; v < n; v++) {
if (g->matrix[u][v] != INF) {
// 更新后继的 ve(取最大值)
int newVe = ve[u] + g->matrix[u][v];
if (newVe > ve[v]) ve[v] = newVe;
inDegree[v]--;
if (inDegree[v] == 0) queue[rear++] = v;
}
}
}
return topoCount; // 返回拓扑序顶点数(< n 说明有环)
}
void criticalPath(Graph* g) {
int n = g->vertexNum;
int ve[MAX_V], vl[MAX_V]; // 最早/最晚发生时间
int topo[MAX_V]; // 拓扑序
// ① 拓扑排序 + 计算 ve
int topoCount = topologicalOrder(g, ve, topo);
if (topoCount < n) {
printf("图中存在环!无法计算关键路径\n");
return;
}
// ② 初始化 vl:所有顶点 = 汇点的 ve
for (int i = 0; i < n; i++) vl[i] = ve[topo[n - 1]];
// ③ 逆拓扑序计算 vl
for (int i = n - 1; i >= 0; i--) {
int u = topo[i];
for (int v = 0; v < n; v++) {
if (g->matrix[u][v] != INF) {
int newVl = vl[v] - g->matrix[u][v];
if (newVl < vl[u]) vl[u] = newVl; // 取最小值
}
}
}
// ④ 输出 ve 和 vl
printf("\n顶点\tve\tvl\n");
for (int i = 0; i < n; i++)
printf("V%d\t%d\t%d\n", i + 1, ve[i], vl[i]);
// ⑤ 输出关键活动
printf("\n关键活动:\n");
printf("活动\t工期\tee\tel\t是否关键\n");
int totalDuration = ve[topo[n - 1]];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (g->matrix[i][j] != INF) {
int ee = ve[i]; // 最早开始
int el = vl[j] - g->matrix[i][j]; // 最晚开始
printf("V%d→V%d\t%d\t%d\t%d\t%s\n",
i + 1, j + 1, g->matrix[i][j], ee, el,
(ee == el) ? "★关键" : "");
}
}
}
printf("\n总工期:%d 天\n", totalDuration);
}
四、测试
cpp
int main() {
Graph g;
initGraph(&g, 6);
addEdge(&g, 0, 1, 3); // V1→V2: 买材料 3天
addEdge(&g, 1, 2, 5); // V2→V3: 打地基 5天
addEdge(&g, 2, 3, 8); // V3→V4: 砌墙 8天
addEdge(&g, 3, 4, 4); // V4→V5: 封顶 4天
addEdge(&g, 1, 5, 6); // V2→V6: 水电 6天
addEdge(&g, 5, 3, 2); // V6→V4: 水电收尾 2天
criticalPath(&g);
return 0;
}
第四部分:图系列总结
| 篇 | 主题 | 核心算法 | 复杂度 |
|---|---|---|---|
| 1 | 图基础 + 遍历 | DFS、BFS | O(n+e) |
| 2 | 最短路径 | Dijkstra、Floyd | O(n²)、O(n³) |
| 3 | 最小生成树 + 拓扑排序 | Prim、Kruskal、Kahn | O(n²)、O(e log e)、O(n+e) |
| 4 | 关键路径 | AOE 网、ve/vl | O(n+e) |
一句话记忆
关键路径是 AOE 网中从源点到汇点的最长路径。ve 正向取最大(最早什么时候能开始),vl 反向取最小(最晚什么时候必须开始)。ee==el 的活动是关键活动,一天都不能拖。总工期等于汇点的 ve。