关键路径法:算法、公式与算例
关键路径是项目中相互依赖的任务里最长的一条链。它的长度就是项目最短可能工期——这条链上每延误一天,完工就晚一天。
基本想法
并非每项任务对工期的影响都一样。有些有余量,可以推迟而不影响完工;有些一点余量都没有——这些构成关键路径。链上的任务叫关键任务,因为它们的浮动时间为零:没有任何富余,所以关键任务延误一天,项目完工日期就一对一地后移一天。
关键路径法(Critical Path Method,CPM)由 Morgan Walker 和 James Kelley 在 1950 年代末做工厂检修排期时提出。它的价值是聚焦:几十项任务里,直接指出卡住完工日期的那几项。
几个必须先厘清的术语
- 关键路径——相互依赖的活动中最长的一条路径,它的长度就是项目最短工期。
- 浮动时间(时差)——一项活动在不推迟完工的前提下可以推迟多久。关键活动为零。
- 正推——从左往右,求每项活动的最早时间。
- 逆推——从右往左,求在仍能按期完工前提下的最晚时间。
- ES / EF——最早开始与最早完成,
EF = ES + 工期。 - LS / LF——最晚开始与最晚完成,
LS = LF − 工期。
把它们串起来:总浮动时间 = LS − ES = LF − EF。两种算法结果必然相同,可以互相校验;结果为零,这项活动就是关键的。
逐步计算:五个步骤
- 列出活动与依赖关系。每项活动、它的工期,以及它的前置任务。
- 画出网络。从唯一的开始节点连到唯一的结束节点。
- 正推求 ES / EF。从左往右:第一项活动 ES = 0,
EF = ES + 工期;后续每项的 ES 取所有前置任务 EF 中的最大值——只要还有一个前置没完成,它就动不了。末端最大的那个 EF 就是项目工期。汇合处取最大值,是手算最常出错的一步。 - 逆推求 LS / LF。从右往左:最后一项活动的 LF 等于项目工期,
LS = LF − 工期;更早每项的 LF 取所有紧后活动 LS 中的最小值——它必须赶在最早需要它的那项之前完成。正推取最大值、逆推取最小值,这一对相反的规则就是全部机关。 - 算浮动时间。
浮动时间 = LS − ES。浮动时间为零的活动首尾相连,就是关键路径。
一个完整算例,连算术一起
一个物流仓库的货架安装工程,九项活动,工期单位是工作日。下面每一个数字都只用到加法和减法。设第 0 天为 3 月 2 日(周一)。
青云仓储 B 库货架安装。九项活动,工作日。
| 活动 | 工期(天) | 前置任务 |
|---|---|---|
| A — 现场勘测 | 4 | — |
| B — 货位布置方案设计 | 6 | A |
| C — 施工图审查与消防报建 | 10 | A |
| D — 货架采购(长周期) | 15 | B |
| E — 库区清空腾挪 | 5 | A |
| F — 地坪修补 | 8 | C、E |
| G — 电气一次配管 | 6 | F |
| H — 货架安装 | 9 | D、G |
| I — 监理验收与竣工验收 | 3 | H |
网络里一共三条路径,各自求和:
- A → C → F → G → H → I = 4 + 10 + 8 + 6 + 9 + 3 = 40 天
- A → B → D → H → I = 4 + 6 + 15 + 9 + 3 = 37 天
- A → E → F → G → H → I = 4 + 5 + 8 + 6 + 9 + 3 = 35 天
40 天最长,答案就是它。下面的正推逆推既是证明,也顺便给其余活动的余量定价。
| 活动 | 工期 | ES = max(前置 EF) | EF | LF = min(紧后 LS) | LS | 总浮动时间 | 关键? |
|---|---|---|---|---|---|---|---|
| A — 现场勘测 | 4 | 0(无前置) | 0+4 = 4 | min(7, 4, 9) = 4 | 4−4 = 0 | 0 | 是 |
| B — 方案设计 | 6 | 4(A) | 4+6 = 10 | 13(D) | 13−6 = 7 | 3 | 否 |
| C — 图审与报建 | 10 | 4(A) | 4+10 = 14 | 14(F) | 14−10 = 4 | 0 | 是 |
| D — 货架采购 | 15 | 10(B) | 10+15 = 25 | 28(H) | 28−15 = 13 | 3 | 否 |
| E — 库区清空 | 5 | 4(A) | 4+5 = 9 | 14(F) | 14−5 = 9 | 5 | 否 |
| F — 地坪修补 | 8 | max(14, 9) = 14 | 14+8 = 22 | 22(G) | 22−8 = 14 | 0 | 是 |
| G — 电气配管 | 6 | 22(F) | 22+6 = 28 | 28(H) | 28−6 = 22 | 0 | 是 |
| H — 货架安装 | 9 | max(25, 28) = 28 | 28+9 = 37 | 37(I) | 37−9 = 28 | 0 | 是 |
| I — 竣工验收 | 3 | 37(H) | 37+3 = 40 | 40(项目结束) | 40−3 = 37 | 0 | 是 |
有两行承载了整个方法。F 从第 14 天开始而不是第 9 天,它等的是图审报建,不是库区清空。H 从第 28 天开始而不是第 25 天:电气这条链比货架到货还晚。A 的 LS 算出来正好是 0,这是校验——大于 0,就说明 40 天不是最短工期。
浮动时间为零的 A、C、F、G、H、I 构成关键路径,恰好是最先加出来的那条 40 天的链。浮动时间也和路径长度对得上:B 和 D 在 37 天那条路径上,40 − 37 = 3;E 在 35 天那条上,40 − 35 = 5。某条路径上的浮动时间,永远等于项目工期减去这条路径的长度——这是校验自己算术最快的办法。
现在让某项延误。图审报建(C)不是 10 天而是 14 天,关键活动上晚了四天。从 C 重跑正推:EF = 4 + 14 = 18,于是 F 的 ES 18 / EF 26,G 的 ES 26 / EF 32,H 的 ES max(25, 32) = 32 / EF 41,I 的 EF 变成 44。
项目一天对一天地推后了——没有任何东西吸收这次延误,因为根本没有可用来吸收的东西。而其余所有人都变富了:从 LF(I) = 44 重跑逆推,B 的浮动时间变 7、D 变 7、E 变 9,各自都长了四天。别处的余量,是关键路径上的延误制造出来的。
再来一次超支浮动时间。把 C 改回 10 天,让货架采购(D,浮动时间 3)从 15 天变成 20 天——在一项非关键活动上晚了五天。D 的 EF 变成 10 + 20 = 30,于是 H 的 ES max(30, 28) = 30 / EF 39,I 在第 42 天完成。
晚两天,不是五天:前三天是从 D 自己的浮动时间里出的,只有超支的那部分传到了终点。浮动时间是一笔预算,只能花一次。而且关键路径移位了——A → B → D → H → I 现在是 4 + 6 + 20 + 9 + 3 = 42 天,原来那条图审链反而多出 2 天浮动时间。你一直在盯的那几项活动,已经不是要紧的那几项了,而如果没有东西替你重算,谁也不会告诉你这件事。
CPM、PERT 与关键链
CPM 每项任务只用一个确定工期,输出关键路径和浮动时间,适合估算比较可靠的场合。
PERT 给三个估计——乐观 O、最可能 M、悲观 P——按 (O + 4M + P) / 6 加权。设计任务估 2、4、12 天,期望工期就是 (2 + 16 + 12) / 6 = 5 天,比"最可能"的 4 天多一天,这一天正是长尾风险的定价。常见做法是用 PERT 估工期,再拿它跑 CPM。
关键链来自约束理论,在 CPM 之上加入资源约束,把各任务里分散的余量集中成放在链尾的缓冲。当决定进度的是共用资源时更合适。
总浮动时间、自由浮动时间与负浮动时间
上文说的"浮动时间"都是总浮动时间:一项活动在项目完工日期移动之前能推迟多久。单看它会误导人,因为总浮动时间常常是共享的。自由浮动时间问的是更严格的问题——这项活动推迟多久才会打扰到任何一个紧后活动?公式是 紧后活动中最早的 ES − 本活动 EF:
- B:总浮动时间 3,自由浮动时间 = ES(D) 10 − EF(B) 10 = 0。B 拖一天,D 立刻就动。那三天属于 B→D 这整条链,谁先花掉就是谁花掉了。
- D:总浮动时间 3,自由浮动时间 = ES(H) 28 − EF(D) 25 = 3。D 的余量确实是 D 自己的。
- E:总浮动时间 5,自由浮动时间 = ES(F) 14 − EF(E) 9 = 5。完全私有,就摆在等报建的那段空档前面。
总浮动时间是交付期能吸收的量,自由浮动时间是你不用打招呼就能花的量。同一条链上的两位负责人如果各自按总浮动时间做打算,他们会把同一批天数各花一遍。
负浮动时间意味着出了问题:最晚开始落在了最早开始之前,通常是因为有人指定的交付期早于逻辑允许的最早日期。−4 不是富余,它读作"这件事本该四天前就开始"。
gantts.app 是怎么算的(以及它和教科书的差别)
这一点值得说准确,因为我们的正推不是上面那一套,而差别就落在浮动时间这一列上。
教科书的 CPM 不管你把横条画在哪:它把每项活动都摆到前置任务允许的最早位置,所以 E 从第 4 天开始,无论你有没有把它摆在那儿。gantts.app 跑的是"按摆放位置"的 CPM——每项任务从它自己被摆放的开始日期起算,前置任务只能把它往后推,永远不会往前拉。写成代码就是一个 max:最早开始先取任务自身的开始,只有当约束要求更晚时,依赖关系才把它抬高。
把 E 的横条摆在第 8 天而不是第 4 天,差别就是一个数字。教科书 CPM 仍然报 ES 4、浮动时间 5 天。我们报 ES 8,而逆推不变(LS 9),浮动时间是 1 天。两个都对,只是回答的问题不同:教科书问的是逻辑允许多少余量,我们问的是这份画出来的计划还剩多少余量。
理由是它首先是一张图:一个会悄悄搬动任务的正推,等于为一份并不在屏幕上的进度计划打印浮动时间。拖动横条是一条指令,我们就当它是指令。
其余部分是常规的。最晚完成取紧后活动最晚开始中的最小值(或项目结束日),浮动时间是 LS − ES,滞后量和非完成-开始的连线都走同一遍推算。浮动时间小于或等于零时任务即为关键,所以负浮动时间会显示出来而不是被抹掉。另有三点:
- 只报总浮动时间。每项任务一个数字,而且是总浮动时间(总浮动时间)——界面上没有自由浮动时间这一列。
- 摘要行不参与排程。CPM 只跑叶子任务和里程碑;分组横条是下属任务的汇总。
- 启用工作日历时按工作日算。推算用的是工作日序号,所以偏移 5 表示五个工作日,紧后任务落在周一而不是周日,法定节假日和调休也一并按日历处理。
想要教科书那种行为——所有任务都拉回逻辑允许的最早日期——点自动排程。它重跑一遍正推,只改一条规则:有前置任务的任务完全由约束驱动,不管它现在摆在哪;没有前置任务的任务原地不动,当锚点。点完之后,两种算法结果一致。
在 gantts.app 里看关键路径
手算一遍是理解 CPM 的必经之路,但你不会想在每次估算变动时都重算一遍——而且上面第二次延误已经说明,路径本身是会移位的。
- 打开 gantts.app,用+ 任务和◆ 里程碑把九项活动加进去,在天数列填工期。
- 在前置任务列直接按行号写依赖关系,多个用逗号分隔,例如 F 那一行写
3,5;也可以从横条边缘的小圆点拖到目标横条上。 - 进工作日历登记法定节假日和调休上班日,工期才按真实工作日展开。
- 点自动排程,把每项任务拉回依赖关系允许的最早日期——这一步之后,结果与教科书 CPM 一致。
- 勾上关键路径,零浮动的那条链会画成斜纹;天数和总浮动时间可在列里显示出来。
- 改一个工期试试:把 C 从 10 改成 14,高亮和日期立刻重算,不用重跑任何一遍手算。
- 计划谈定后存一版基准,再用⬇ 导出出 PDF、PNG、Excel 或 PowerPoint。
它还告诉你精力花在哪里有效:给 E 加人对完工毫无帮助,因为它有 5 天浮动时间;在 C 上省一天,项目就真的早一天完工。关键路径不是计划的固有属性,而是某一时刻的快照,每周更新后都要重算——手算九项尚可,六十项就不现实。
刚接触排程的话,先读什么是甘特图,再看甘特图怎么做,或者直接从模板库拿一份现成计划。
常见问题
什么是关键路径?
相互依赖的任务中最长的一条链。它的长度等于项目最短工期,链上任务没有浮动时间,因此其中任何一项延误一天,完工就晚一天。
关键路径怎么计算?
先列出所有任务的工期和前置任务,再正推求最早开始与最早完成(汇合处取最大值),然后从项目工期逆推求最晚完成与最晚开始(分叉处取最小值),最后用最晚开始减最早开始得到浮动时间。浮动时间为零的任务首尾相连,就是关键路径。
什么是总浮动时间?
一项任务在不推迟项目完工的前提下可以推迟的时间,等于最晚开始减最早开始,也等于最晚完成减最早完成。注意它由整条支路共享:前面的任务用掉了,后面就没有了。
正推和逆推有什么区别?
正推从项目开始向后走,算每项任务的最早开始和最早完成,最后得到项目最短工期;逆推从这个工期往回走,算在不推迟完工的前提下每项任务最晚能什么时候开始和完成。两者之差就是浮动时间。
CPM 和 PERT 有什么区别?
CPM 每项任务用一个确定工期,关注关键路径和浮动时间;PERT 用乐观、最可能、悲观三个估计按 (O + 4M + P) / 6 加权,用来处理工期不确定性。实际项目里常常先用 PERT 估工期,再用 CPM 排关键路径。
关键路径可能有多条吗?
可能。在依赖密集的计划中,多条并行路径长度相同,于是全部成为关键路径。这时项目没有任何缓冲,任何一条路径上的延误都会直接体现在完工日期上,风险显著上升。
总浮动时间和自由浮动时间有什么区别?
总浮动时间是项目完工日期移动之前一项活动能推迟多久;自由浮动时间是它打扰到任何一个紧后活动之前能推迟多久,等于紧后活动中最早的 ES 减去本活动的 EF。总浮动时间沿整条链共享,自由浮动时间是这项活动私有的。gantts.app 只报总浮动时间,界面上没有自由浮动列。
gantts.app 算出来的浮动时间为什么和我手算的不一样?
因为它跑的是"按摆放位置"的 CPM:每项任务从你摆放的开始日期起算,依赖关系只能把它往后推,永远不会往前拉。教科书 CPM 则把每项任务都摆到逻辑允许的最早位置。想让两者一致,点"自动排程",它会把所有有前置任务的任务拉回最早合法日期。
负浮动时间是什么意思?
最晚开始落在最早开始之前,通常是因为指定的交付期早于逻辑允许的最早完工日。−4 不是富余,它表示这件事本该四天前就开始。gantts.app 把浮动时间小于或等于零的任务都标为关键,所以负浮动会显示出来而不是被抹掉。
延伸阅读
本文也提供英文版本。