用Octave计算关键路径法(CPM)问题
广告
{{v.name}}
关键路径法(Critical Path Method, CPM)是项目进度管理的核心运筹学方法,核心是通过分析项目中各活动的时间依赖关系,计算关键时间参数、识别关键活动和关键路径,最终确定项目的最短完成工期,并明确哪些活动的延迟会直接导致总工期延长(关键活动),为项目进度管控、资源调配提供依据。
CPM的核心前提是活动持续时间为确定值(若持续时间随机,用计划评审技术PERT),计算围绕正推法(算最早时间)和逆推法(算最迟时间)展开,最终通过总时差判定关键路径,以下从核心概念、手动计算步骤、Octave代码实现、扩展场景全维度讲解,零基础可直接落地。
一、CPM核心基础概念
1. 项目活动分解与表示
将项目拆解为若干独立活动,明确活动的持续时间和先后依赖关系(紧前/紧后活动),常用活动节点图(AON) 表示(节点=活动,箭线=依赖关系,最通用),也可用箭线图(AOA,箭线=活动),本文以AON为例。
2. 5个核心时间参数
所有计算围绕这5个参数展开,是判定关键路径的基础,设活动\(i\)的持续时间为\(t_i\):
| 参数 | 符号 | 核心含义 | 计算公式 |
|--------------|------|--------------------------------------------------------------------------|------------------------------|
| 最早开始时间 | \(ES\) | 活动最早能开始的时间(需所有紧前活动全部完成)| 紧前活动的\(EF\)最大值 |
| 最早完成时间 | \(EF\) | 活动最早能完成的时间 | \(EF = ES + t\) |
| 最迟完成时间 | \(LF\) | 活动最迟必须完成的时间(不延误项目总工期)| 紧后活动的\(LS\)最小值 |
| 最迟开始时间 | \(LS\) | 活动最迟必须开始的时间 | \(LS = LF - t\) |
| 总时差 | \(TF\) | 活动的最大可延迟时间(延迟不影响项目总工期)| \(TF = LS - ES = LF - EF\) |
3. 关键路径判定规则
- 关键活动:总时差\(TF=0\)的活动(无任何缓冲时间,延迟1天则项目总工期延迟1天);
- 关键路径:由所有关键活动按依赖关系组成的路径,是项目的最长路径(路径总持续时间=项目最短完成工期);
- 项目可存在多条关键路径,所有关键路径的总持续时间相同,均需重点管控。
4. 辅助概念
- 紧前活动:某活动开始前必须完成的前置活动;
- 紧后活动:某活动完成后才能开始的后续活动;
- 虚活动:仅表示依赖关系,持续时间为0的活动(用于处理复杂依赖,如多紧前/紧后)。
二、CPM手动计算步骤(核心:正推+逆推)
手动计算是理解CPM的基础,适用于中小型项目(活动数<20),步骤为定义活动清单→正推法算\(ES/EF\)→逆推法算\(LF/LS\)→计算总时差→判定关键路径,结合经典示例一步一步落地。
例子
某项目拆解为6个活动,活动清单、持续时间、紧前活动如下:
| 活动代号 | 持续时间\(t\)(天) | 紧前活动 |
|----------|-------------------|----------|
| A | 3 | 无 |
| B | 2 | A |
| C | 4 | A |
| D | 3 | B |
| E | 2 | C |
| F | 1 | D,E |
步骤1:正推法计算\(ES\)、\(EF\)(从项目起点→终点,顺向计算)
核心规则:
1. 无紧前活动的起始活动,\(ES=0\)(默认项目从第0天开始);
2. 非起始活动的\(ES = \max(\text{所有紧前活动的}EF)\)(必须等最晚完成的紧前活动结束);
3. 所有活动的\(EF = ES + 本活动持续时间t\)。
计算过程:
| 活动 | \(t\) | 紧前活动 | \(ES\) | \(EF=ES+t\) |
|------|-----|----------|------------|-----------|
| A | 3 | 无 | 0 | 3 |
| B | 2 | A | 3 | 5 |
| C | 4 | A | 3 | 7 |
| D | 3 | B | 5 | 8 |
| E | 2 | C | 7 | 9 |
| F | 1 | D,E | max(8,9) =9|10 |
关键结论:项目最短完成工期 = 最终活动的\(EF\) = 10天。
步骤2:逆推法计算\(LF\)、\(LS\)(从项目终点→起点,逆向计算)
核心规则:
1. 无紧后活动的结束活动,\(LF=\)项目最短完成工期;
2. 非结束活动的\(LF = \min(\text{所有紧后活动的}LS)\)(必须等最早开始的紧后活动开始);
3. 所有活动的\(LS = LF - 本活动持续时间t\)。
计算过程:
| 活动 | \(t\) | 紧后活动 | \(LF\) | \(LS=LF-t\) |
|------|-----|----------|------------|-----------|
| F | 1 | 无 | 10 | 9 |
| E | 2 | F | 9 | 7 |
| D | 3 | F | 9 | 6 |
| C | 4 | E | 7 | 3 |
| B | 2 | D | 6 | 4 |
| A | 3 | B,C | min(4,3)=3|0 |
步骤3:计算总时差\(TF\),判定关键路径
计算每个活动的总时差\(TF = LS - ES\),若\(TF=0\)则为关键活动,所有关键活动组成关键路径。
计算过程
| 活动 | \(ES\) | \(EF\) | \(LS\) | \(LF\) | \(TF=LS-ES\) | 关键活动 |
|------|------|------|------|------|-------------|----------|
| A | 0 | 3 | 0 | 3 | 0 | 是 |
| B | 3 | 5 | 4 | 6 | 1 | 否 |
| C | 3 | 7 | 3 | 7 | 0 | 是 |
| D | 5 | 8 | 6 | 9 | 1 | 否 |
| E | 7 | 9 | 7 | 9 | 0 | 是 |
| F | 9 | 10 | 9 | 10 | 0 | 是 |
关键路径:A → C → E → F,总持续时间=10天。
步骤4:项目管控结论
1. 关键活动A、C、E、F无缓冲时间,需重点管控,确保按计划完成;
2. 非关键活动B、D各有1天总时差,可延迟1天(或调配资源至关键活动),不影响总工期;
3. 若需缩短项目总工期,只能压缩关键路径上活动的持续时间(压缩非关键活动无意义)。
代码如下:
function result = cpm_calculation(activities, durations, predecessors)
% 关键路径法计算函数
% 输入:
% activities: 活动代号数组,cell类型,如{'A','B','C'}
% durations: 活动持续时间数组,数值型,如[3,2,4]
% predecessors: 紧前活动索引数组,每行对应一个活动的紧前活动索引(索引从1开始,空为[]),如[[],[1],[1],[2],[3],[4,5]]
% 输出:
% result: 结构体,包含所有时间参数、总工期、关键活动、关键路径
n = length(activities);
if length(durations) ~= n || length(predecessors) ~= n
error('输入参数长度不匹配');
end
% 初始化
ES = zeros(1, n);
EF = zeros(1, n);
LS = inf(1, n);
LF = inf(1, n);
Slack = zeros(1, n);
% 构建后继列表
successors = cell(1, n);
for i = 1:n
for j = 1:n
if ismember(i, predecessors{j})
successors{i} = [successors{i}, j];
end
end
end
% 前向计算:最早开始和完成时间
% 先设置起点
start_indices = find(cellfun(@isempty, predecessors));
ES(start_indices) = 0;
EF(start_indices) = durations(start_indices);
changed = true;
while changed
changed = false;
for i = 1:n
if isempty(predecessors{i})
new_ES = 0;
else
pred_EF = EF(predecessors{i});
new_ES = max(pred_EF);
end
if new_ES > ES(i)
ES(i) = new_ES;
EF(i) = ES(i) + durations(i);
changed = true;
end
end
end
% 总工期
total_duration = max(EF);
% 后向计算:最晚开始和完成时间
% 先设置终点
end_indices = find(cellfun(@isempty, successors));
LF(end_indices) = total_duration;
LS(end_indices) = LF(end_indices) - durations(end_indices);
changed = true;
while changed
changed = false;
for i = 1:n
if isempty(successors{i})
new_LF = total_duration;
else
succ_LS = LS(successors{i});
new_LF = min(succ_LS);
end
if new_LF < LF(i)
LF(i) = new_LF;
LS(i) = LF(i) - durations(i);
changed = true;
end
end
end
% 计算松弛时间
Slack = LS - ES;
% 关键活动
critical_activities_indices = find(Slack == 0);
critical_activities = activities(critical_activities_indices);
% 关键路径:从起点到终点,连接关键活动
% 起点:没有紧前的活动
start_indices = find(cellfun(@isempty, predecessors));
% 终点:没有后继的活动
end_indices = find(cellfun(@isempty, successors));
% 找到关键路径:从关键起点开始,沿着关键后继
critical_path = {};
if ~isempty(critical_activities_indices)
% 找到关键起点
critical_starts = intersect(start_indices, critical_activities_indices);
if ~isempty(critical_starts)
current = critical_starts(1);
critical_path = {activities{current}};
visited = [current];
while true
% 找到当前的关键后继
nexts = successors{current};
next_critical = intersect(nexts, critical_activities_indices);
next_critical = setdiff(next_critical, visited);
if isempty(next_critical)
break;
end
current = next_critical(1);
critical_path = [critical_path, activities{current}];
visited = [visited, current];
end
end
end
% 构建结果结构体
result.ES = ES;
result.EF = EF;
result.LS = LS;
result.LF = LF;
result.Slack = Slack;
result.total_duration = total_duration;
result.critical_activities = critical_activities;
result.critical_path = critical_path;
end
结果如下:
# test_cpm.m
activities = {'A', 'B', 'C', 'D', 'E', 'F'};
durations = [3, 2, 4, 3, 2, 1];
predecessors = {[], [1], [1], [2], [3], [4, 5]};
result = cpm_calculation(activities, durations, predecessors);
disp('最早开始时间 (ES):');
disp(result.ES);
disp('最早完成时间 (EF):');
disp(result.EF);
disp('最晚开始时间 (LS):');
disp(result.LS);
disp('最晚完成时间 (LF):');
disp(result.LF);
disp('松弛时间 (Slack):');
disp(result.Slack);
disp('总工期:');
disp(result.total_duration);
disp('关键活动:');
disp(result.critical_activities);
disp('关键路径:');
disp(result.critical_path);
>> test_cpm
最早开始时间 (ES):
0 3 3 5 7 9
最早完成时间 (EF):
3 5 7 8 9 10
最晚开始时间 (LS):
0 4 3 6 7 9
最晚完成时间 (LF):
3 6 7 9 9 10
松弛时间 (Slack):
0 1 0 1 0 0
总工期:
10
关键活动:
{
[1,1] = A
[1,2] = C
[1,3] = E
[1,4] = F
}
关键路径:
{
[1,1] = A
[1,2] = C
[1,3] = E
[1,4] = F
}