关键路径解析:项目管理的工期奥秘

引言

上一篇我们学习了最小生成树(用最小代价连接所有点)和拓扑排序(给有依赖关系的任务排序)。今天要讲的是图系列的最后一篇------关键路径

假如你是一个项目经理,要盖一栋楼。有些工序必须按顺序来(先打地基才能砌墙),有些可以同时进行(水电和装修可以并行)。现在老板问你:最快多久能完工?哪些工序一天都不能拖延?

这就是关键路径问题------在一个项目中,找出耗时最长的任务链。这条路径上的任务一旦延期,整个项目就会延期。

第一部分: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。

相关推荐
love_muming2 小时前
链表每日一练
java·开发语言·数据结构·链表·idea·每日一练
玖玥拾2 小时前
C/C++ 数据结构(二)双向链表
c语言·数据结构·c++
乐观勇敢坚强的老彭2 小时前
GESP一级核心算法:循环与条件判断的结合
java·数据结构·算法
noipp2 小时前
推荐题目:洛谷 P1737 [NOI2016] 旷野大计算
linux·数据结构·算法
lzjava20242 小时前
Python的数据结构,推导式、迭代器和生成器
数据结构·windows·python
rit84324994 小时前
MATLAB近红外光谱预处理:平滑与求导(MSV方法)
数据结构·算法·matlab
触底反弹4 小时前
从 JS 引擎执行原理理解数据类型:栈内存、堆内存与作用域
javascript·数据结构·面试
郝学胜_神的一滴4 小时前
干货版《算法导论》09:让哈希表稳如泰山的终极解法
数据结构·算法
洛水水4 小时前
【力扣100题】88.多数元素
数据结构·算法·leetcode