Appearance
关键路径(AOE 网)
2026 大纲 五(四)图的基本应用 4. 关键路径(前置是《拓扑排序》,AOV 与 AOE 的对照表也在那一篇)。
工期为什么等于最长路径
AOE 网是一个带权 DAG:顶点是事件("射入它的活动全部完成、射出它的活动可以开始"这个状态),边是活动,边权是活动的持续时间。
它比一般 DAG 多一条硬约束:
🔴 入度为 0 的顶点和出度为 0 的顶点各有且仅有一个——工程只有一个起始时刻(源点)和一个完工时刻(汇点)。AOV 网可以有多个,AOE 不行。
现在问"这项工程最短要多久"。直觉可能会说"最短当然是找最短的那条路",但那是错的。完工的定义是"所有活动都结束"——几条并行的路径同时在推进,工程要等最慢的那条走完才算完。所以:
🔴 最短工期 = 源点到汇点的最长带权路径长度。 这条最长路径就叫关键路径。
⚠️ 这里有个反直觉的地方要坐实:"最短工期"和"最长路径"这两个词看着矛盾,其实说的是同一件事——工期由最慢的那条支路决定,而你没法比它更快。
顺带一提:一般图上求最长路径是 NP-难的,但 DAG 上按拓扑序做一次线性 DP 就够了,这就是下面第一步在做的事。
先动手看一眼
盯
四个量:两个在顶点上,两个在边上
| 符号 | 名称 | 属于 | 计算方式 | 为什么 |
|---|---|---|---|---|
| 事件 | 顶点 | 对所有前驱 | 射入 | |
| 事件 | 顶点 | 对所有后继 | 不能耽误任何一个后继,最紧的说了算 | |
| 活动 | 边 | 起点事件发生后才能开工 | ||
| 活动 | 边 | 须在终点事件最迟发生前干完 | ||
| 时间余量 | 边 | 🔴 |
记住"
等所有前驱到齐、 迁就最赶时间的后继"这两句,就不会把 max 和 min 取反。
为什么必须先拓扑排序? 因为算
手算的第一自检:算完
算法实现
c
int ve[MAXV], vl[MAXV]; // 事件最早 / 最迟发生时间
int topoOrder[MAXV], topoCnt; // 拓扑序列,第二步要倒着用
// topoSort() 就是《拓扑排序》那篇的 Kahn 算法,只多做两件事:把出队顺序记进
// topoOrder[],并在处理弧 <u,v> 时顺手正推
// if (ve[u] + edges[i].w > ve[v]) ve[v] = ve[u] + edges[i].w; // 正推取 max
// 返回 0 表示有环。完整代码见下方折叠块。
void criticalPath(void) {
if (!topoSort()) { printf("图中有环,无关键路径\n"); return; }
int sink = topoOrder[topoCnt - 1]; // 拓扑序最后一个顶点就是汇点
for (int i = 0; i < n; i++) vl[i] = ve[sink]; // 先全置成总工期,再由 min 压下来
for (int idx = topoCnt - 1; idx >= 0; idx--) { // 逆拓扑序倒推 vl[]
int u = topoOrder[idx];
for (int i = head[u]; i != -1; i = edges[i].next)
if (vl[edges[i].to] - edges[i].w < vl[u]) // 倒推取 min
vl[u] = vl[edges[i].to] - edges[i].w;
}
printf("工程最短工期 = %d\n", ve[sink]);
for (int u = 0; u < n; u++) // e == l 即关键活动
for (int i = head[u]; i != -1; i = edges[i].next)
if (ve[u] == vl[edges[i].to] - edges[i].w)
printf("关键活动 <%d, %d>\n", u, edges[i].to);
}时间与空间都是
缩短工期:三条结论,条条有反例
这是这一节被考得最多的地方,而且三条结论全都反直觉。
结论一:缩短非关键活动,工期一点也不会变。 它本来就有余量,缩了只是余量变大。
结论二:关键路径可能不止一条,只缩短其中一条上的活动,工期同样不变。 假设有两条并行的关键路径,你把第一条上某个活动缩短了,第二条仍然是原来那么长,工期由它顶着,纹丝不动。
🔴 要缩短工期,必须动"所有关键路径的公共关键活动"。
结论三:缩短公共关键活动有效,但有下限。 缩到某条原本的非关键路径追平时,那条路径就升级成了新的关键路径,工期就此卡死,再缩就是无效投入。
⚠️ 由此可知,"缩短任一关键活动就会缩短工期"是错的,真题把它作为错误选项放过。而反过来的那一半是对的:
🔴 延长关键活动一定延长工期。 工期是一个最大值——对"变大"敏感、对"变小"迟钝。
判两个活动能否同时进行
这是一个不那么显眼、但真题问过的考法:"与活动 X 同时进行的活动可能有哪些"。
注意"可能"这个词——问的不是"必然同时",而是"存不存在某个合法调度让两者重叠"。所以做法是比较两个活动各自的可活动时间窗能否相交。
活动
活动
与活动 可能同时进行 且 (两个开区间有正长度的交集)。
关键活动的窗口是锁死的:
实操上,先把每个活动的
某个活动延误了,怎么补救
真题大题的最后一问常常是这类"扰动分析",有两种问法,做法不同:
问法一:活动
推迟一个活动的开工时刻,不改变其他任何顶点的
一步就出来。
问法二:不改
这一问要重算。
所以步骤是:① 把
topoSort 的完整代码与三处实现细节(照着敲、或想弄清两个初值为什么这样取时展开)
c
int topoSort(void) { // 拓扑排序 + 正推 ve[];返回 0 表示有环
int queue[MAXV], front = 0, rear = 0;
topoCnt = 0;
memset(ve, 0, sizeof(ve));
for (int i = 0; i < n; i++)
if (inDeg[i] == 0) queue[rear++] = i;
while (front < rear) {
int u = queue[front++];
topoOrder[topoCnt++] = u;
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; // 正推取 max
if (--inDeg[v] == 0) queue[rear++] = v;
}
}
return topoCnt == n;
}ve[]全置 0 是对的:权值非负,源点的本就是 0,其余会被 max逐步抬到位;若允许负权(AOE 网中不会出现)就必须初始化为负无穷。vl[]先全置成总工期再由 min 压下来,等价于"任何事件都不允许晚于总工期发生";初值取得比总工期小,某些会被压过头。 head用表示空链,所以要 memset(head, -1, ...):memset按字节填充,的每个字节都是 0xFF,恰好得到全的 int 数组,换成别的值就不能这样填。
七事件十活动的完整手算走查(想手动模拟一遍就展开)
活动清单(起点 → 终点,持续时间):
| 活动 | 边 | 活动 | 边 | ||
|---|---|---|---|---|---|
| 4 | 3 | ||||
| 2 | 2 | ||||
| 2 | 3 | ||||
| 3 | 1 | ||||
| 4 | 4 |
源点是
第一步:按拓扑序正推
| 事件 | 计算过程 | |
|---|---|---|
| 源点 | 0 | |
| 4 | ||
| 2 | ||
| 6 | ||
| 8 | ||
| 9 | ||
| 13 |
工程最短工期
第二步:按逆拓扑序倒推
| 事件 | 计算过程 | |
|---|---|---|
| 汇点, | 13 | |
| 9 | ||
| 12 | ||
| 6 | ||
| 2 | ||
| 4 | ||
| 0 |
第三步:逐活动算
| 活动 | 边 | 余量 | 关键活动? | ||||
|---|---|---|---|---|---|---|---|
| 4 | 0 | 0 | 4 | 是 | |||
| 2 | 0 | 0 | 2 | 是 | |||
| 2 | 4 | 0 | 6 | 是 | |||
| 3 | 4 | 5 | 12 | ||||
| 4 | 2 | 0 | 6 | 是 | |||
| 3 | 2 | 4 | 9 | ||||
| 2 | 6 | 4 | 12 | ||||
| 3 | 6 | 0 | 9 | 是 | |||
| 1 | 8 | 4 | 13 | ||||
| 4 | 9 | 0 | 13 | 是 |
最右边多加的
第四步:连出关键路径。关键活动
,长度 ,长度
自检:两条关键路径长度必须相等(都等于总工期 13)✓;所有关键活动的
顺手做几个变式:
- 余量最大的活动:查
那一列,是 ,余量 5。 - 与
(关键活动,占 )可能同时进行的有哪些:查每个活动是否满足 且 。 ( , )✓; ( , )✓; ( , )✓; ( , )✓。而 ( )✗、 ( )✗。 推迟到时刻 2 才开始,最多能持续多久: 。
缩短工期的定量分析:六条路径长度表与逐值验算(想把三条结论算一遍就展开)
先把这张网里全部 6 条路径的长度列出来:
| 路径 | 活动序列 | 长度 |
|---|---|---|
| 13(关键) | ||
| 13(关键) | ||
| 9 | ||
| 9 | ||
| 9 | ||
| 8 |
有了这张表,三个结论就都是算出来的而不是背出来的:
结论一:缩短非关键活动,工期一点也不会变。 把
结论二:只缩短一条关键路径上的活动,工期同样不变。
结论三:缩短公共关键活动有效,但存在下限。
| 缩短量 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 经过 | 13 | 12 | 11 | 10 | 9 |
| 不经过 | 9 | 9 | 9 | 9 | 9 |
| 实际工期 | 13 | 12 | 11 | 10 | 9 |
同一条路径上多个活动的余量能不能各用各的(想弄清余量到底怎么用就展开)
不可以,余量是共享的,不能相加。
- 若
推迟 4 天(在 开工,12 完工), 就被推到 12, 的最早开始时间随之变成 12——它的余量已经被 用光,归零了。 - 此时
若再推迟 4 天(16 开工、17 完工),整个工程就拖到 17,工期被延误 4 天。
所以"这条路径上有两个余量 4 的活动"不等于"这条路径可以富余 8 天"。看余量要看路径,不能逐个活动累加。
复杂度的逐项来历,以及教材里 AOE 网的邻接表长什么样
| 操作 | 时间 | 来历 |
|---|---|---|
| 拓扑排序 + 正推 | 每个顶点入队出队各一次,每条边处理一次 | |
| 逆拓扑序倒推 | 沿记录下的拓扑序反向扫一遍,每条边再处理一次 | |
| 逐活动求 | 每条边一次 | |
| 总体 | 与拓扑排序同阶 |
空间 ve[]、vl[]、topoOrder[]、队列各

图注:左边是一个 9 事件、11 活动的 AOE 网,顶点 0 是源点(标"开始")、顶点 8 是汇点(标"结束"), 边上的
就是各活动的持续时间。 右边是它的邻接表:顶点表的 count列存入度(拓扑排序用), 边结点除了终点dest还多了一个dur字段存活动时长—— 正推时读的就是这个 dur。 与拓扑排序那篇的 AOV 邻接表对比:结构完全一样,只多了一个dur列。 图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图8.30 一个 AOE 网络及其邻接表表示,p388
考点速记
三条结论:
- 工期 = 源点到汇点的最长路径;
按拓扑序正推取 max, 按逆拓扑序倒推取 min(初值 )。 - 关键活动的判据是
(余量 );关键路径可能不唯一。 - 缩短工期只能动"所有关键路径的公共关键活动",且有下限;而延长关键活动一定延长工期。
这一节在真题里被考过的形式(下方「真题练习」逐题对应):
- 求某个活动的最早、最迟开始时间:直接套
、 。前提是把 、 两趟都算完。 - 求时间余量最大的活动:列出全部活动的
,取最大。这道题问过两次(一次选择、一次大题分问)。 - 加快哪些活动可以缩短工期:选项给的是一对活动,因为有两条关键路径,只加快一条上的没用。做法是先找出全部关键路径,再看哪一对能同时覆盖它们。
- AOE 网叙述的真伪判断:关键路径是"路径长度最长"的(对)还是"边数最多"的(错);"增加任一关键活动的时间不会延长工期"(错);"缩短任一关键活动的时间将会缩短工期"(错,这是四个选项里最迷惑的一个)。
- 由压缩存储的邻接矩阵还原图再求关键路径(大题):先把按行优先压成一维数组的上三角还原成矩阵、画出带权有向图,再在这张图上求关键路径及其长度。
- AOE 网的四问综合大题:① 最短时间与关键活动;② 与某活动可能同时进行的活动有哪些(比时间窗
是否相交);③ 余量最大的活动及其余量;④ 扰动分析——某活动推迟到时刻 开始,为不延期它最多能持续多久( );若不改它的时长,该压缩哪个活动(重算 ,压当前最长路径上的)。
易错:"缩短任一关键活动就能缩短工期"是错的。 关键路径可能有多条,只缩其中一条上的活动,另一条照样顶着。要缩必须动公共关键活动,而且缩到某条非关键路径追平就失效。
易错:"关键路径是边数最多的路径"是错的。 是带权路径长度最长的,与边数无关。
易错:余量不能沿路径累加。 同一条路径上两个活动各有 4 的余量,不等于这条路径能富余 8——前一个用掉了,后一个就归零。
易错:
取 max、 取 min,别取反。 记法:" 等所有前驱到齐(最晚的说了算), 迁就最赶时间的后继(最紧的说了算)"。算完必查 。
教材出处
- 严蔚敏《数据结构(C 语言版)》(第 2 版)印刷 p178–p179,§6.6.4: "与 AOV-网相对应的是 AOE-网 (Activity On Edge),即以边表示活动的网。 AOE-网是一个带权的有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的时间。"
- 同书印刷 p179: "由于整个工程只有一个开始点和一个完成点,故在正常的情况(无环)下, 网中只有一个入度为零的点,称作源点,也只有一个出度为零的点,称作汇点"; "要估算整项工程完成的最短时间,就是要找一条从源点到汇点的带权路径长度最长的路径, 称为关键路径 (Critical Path)"; 同页给出
与 两条递推式, 并明确"求 的值,可根据拓扑顺序从源点开始向汇点递推"、 "求出 后,可根据逆拓扑顺序从汇点开始向源点递推,求出 "。 - 同书印刷 p180:
、 ; "显然,对于关键活动而言, 。对于非关键活动, 的值是该工程的期限余量, 在此范围内的适度延误不会影响整个工程的工期"; 同页给出关键路径求解的五步过程,并指出"关键路径有可能不止一条"。 - 同书印刷 p179:以图 6.28 那个 11 项活动的 AOE 网为例, "关键路径有两条……长度均为 18",是"关键路径不唯一"的教材实例。
- 同书印刷 p178:拓扑排序算法"亦是下面讨论的求关键路径算法的基础"。
图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图8.30,p388。
相关知识
拓扑排序(关键路径的前置;AOV 与 AOE 的对照表在那一篇)| DAG 描述表达式(DAG 的另一类应用)| BFS(正推