【数据结构与算法】第41篇:图论(五):拓扑排序与关键路径

目录

一、AOV网与拓扑排序

[1.1 AOV网的定义](#1.1 AOV网的定义)

[1.2 拓扑排序](#1.2 拓扑排序)

[1.3 算法思想(Kahn算法)](#1.3 算法思想(Kahn算法))

[1.4 代码实现](#1.4 代码实现)

二、AOE网与关键路径

[2.1 AOE网的定义](#2.1 AOE网的定义)

[2.2 关键路径概念](#2.2 关键路径概念)

[2.3 算法步骤](#2.3 算法步骤)

[2.4 代码实现](#2.4 代码实现)

三、AOV网与AOE网对比

四、关键路径的实际意义

五、小结

六、思考题


一、AOV网与拓扑排序

1.1 AOV网的定义

AOV网(Activity On Vertex Network):用顶点表示活动,用有向边表示活动之间的先后关系。

  • u → v 表示活动 u 必须在活动 v 之前完成

  • AOV网必须是有向无环图(DAG)

示例(课程先修关系):

text

复制代码
高等数学 → 离散数学 → 数据结构 → 算法设计
         ↘           ↘
          线性代数 → 数值分析

1.2 拓扑排序

拓扑排序 :将DAG中所有顶点排成一个线性序列,使得对每条边 u → v,u 都在 v 之前出现。

应用

  • 课程安排

  • 编译依赖(Makefile)

  • 任务调度

1.3 算法思想(Kahn算法)

  1. 计算所有顶点的入度

  2. 将所有入度为0的顶点入栈(或队列)

  3. 弹出顶点,加入拓扑序列,将其所有邻接点的入度减1

  4. 若邻接点入度变为0,入栈

  5. 重复直到栈空

判断是否有环:如果输出的顶点数 < 总顶点数,说明图中有环。

1.4 代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_VERTICES 100

// 邻接表节点
typedef struct EdgeNode {
    int vertex;
    struct EdgeNode *next;
} EdgeNode;

typedef struct {
    EdgeNode *first;
} VertexNode;

typedef struct {
    VertexNode vertices[MAX_VERTICES];
    int vertexCount;
} Graph;

// 栈结构
typedef struct {
    int data[MAX_VERTICES];
    int top;
} Stack;

void initStack(Stack *s) { s->top = -1; }
int isEmpty(Stack *s) { return s->top == -1; }
void push(Stack *s, int val) { s->data[++s->top] = val; }
int pop(Stack *s) { return s->data[s->top--]; }

// 初始化图
void initGraph(Graph *g, int n) {
    g->vertexCount = n;
    for (int i = 0; i < n; i++) {
        g->vertices[i].first = NULL;
    }
}

// 添加边 u -> v
void addEdge(Graph *g, int u, int v) {
    EdgeNode *e = (EdgeNode*)malloc(sizeof(EdgeNode));
    e->vertex = v;
    e->next = g->vertices[u].first;
    g->vertices[u].first = e;
}

// 拓扑排序
int topologicalSort(Graph *g, int result[]) {
    int indegree[MAX_VERTICES] = {0};
    
    // 1. 计算入度
    for (int i = 0; i < g->vertexCount; i++) {
        EdgeNode *p = g->vertices[i].first;
        while (p != NULL) {
            indegree[p->vertex]++;
            p = p->next;
        }
    }
    
    // 2. 入度为0的顶点入栈
    Stack stack;
    initStack(&stack);
    for (int i = 0; i < g->vertexCount; i++) {
        if (indegree[i] == 0) {
            push(&stack, i);
        }
    }
    
    // 3. 处理
    int index = 0;
    while (!isEmpty(&stack)) {
        int u = pop(&stack);
        result[index++] = u;
        
        EdgeNode *p = g->vertices[u].first;
        while (p != NULL) {
            int v = p->vertex;
            indegree[v]--;
            if (indegree[v] == 0) {
                push(&stack, v);
            }
            p = p->next;
        }
    }
    
    // 判断是否有环
    if (index != g->vertexCount) {
        printf("图中有环,无法进行拓扑排序\n");
        return 0;
    }
    return 1;
}

int main() {
    Graph g;
    initGraph(&g, 6);
    
    // 构建DAG
    addEdge(&g, 5, 2);
    addEdge(&g, 5, 0);
    addEdge(&g, 4, 0);
    addEdge(&g, 4, 1);
    addEdge(&g, 2, 3);
    addEdge(&g, 3, 1);
    
    int result[MAX_VERTICES];
    printf("拓扑排序序列: ");
    if (topologicalSort(&g, result)) {
        for (int i = 0; i < g.vertexCount; i++) {
            printf("%d ", result[i]);
        }
        printf("\n");
    }
    
    return 0;
}

运行结果:

text

复制代码
拓扑排序序列: 4 5 2 0 3 1 

二、AOE网与关键路径

2.1 AOE网的定义

AOE网(Activity On Edge Network):用边表示活动,用顶点表示事件。

  • 事件:某个时间点,表示它前面的活动已完成

  • 活动:从事件i到事件j,耗时w

  • 源点:入度为0的顶点(工程开始)

  • 汇点:出度为0的顶点(工程结束)

示例(项目进度):

text

复制代码
      2       3
   v1 → v2 → v4
   |    ↗    |
 4 |   /     | 2
   |  /1     |
   v ↙       ↓
   v3 → → → v5
        5

2.2 关键路径概念

  • 最早开始时间(ve):事件最早发生时间

  • 最晚开始时间(vl):事件最晚发生时间(不影响工期)

  • 关键路径:ve == vl 的顶点组成的路径

  • 关键活动:在关键路径上的活动

关键路径上的活动:任何延迟都会导致整个工程延期。

2.3 算法步骤

  1. 拓扑排序:得到顶点顺序

  2. 计算ve :按拓扑序,ve[j] = max(ve[i] + w(i,j))

  3. 计算vl :按逆拓扑序,vl[i] = min(vl[j] - w(i,j)),汇点vl=ve

  4. 判断关键活动 :对边 i→j,若 ve[i] == vl[j] - w(i,j),则为关键活动

2.4 代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

#define MAX_VERTICES 100
#define INF INT_MAX

// 边结构
typedef struct Edge {
    int to;
    int weight;
    struct Edge *next;
} Edge;

// 图结构
typedef struct {
    Edge *heads[MAX_VERTICES];
    int vertexCount;
} Graph;

// 入度数组(拓扑排序用)
int indegree[MAX_VERTICES];

void initGraph(Graph *g, int n) {
    g->vertexCount = n;
    for (int i = 0; i < n; i++) {
        g->heads[i] = NULL;
        indegree[i] = 0;
    }
}

void addEdge(Graph *g, int from, int to, int weight) {
    Edge *e = (Edge*)malloc(sizeof(Edge));
    e->to = to;
    e->weight = weight;
    e->next = g->heads[from];
    g->heads[from] = e;
    indegree[to]++;
}

// 拓扑排序(同时返回拓扑序)
int topologicalSort(Graph *g, int topo[]) {
    int stack[MAX_VERTICES];
    int top = -1;
    
    for (int i = 0; i < g->vertexCount; i++) {
        if (indegree[i] == 0) {
            stack[++top] = i;
        }
    }
    
    int index = 0;
    while (top != -1) {
        int u = stack[top--];
        topo[index++] = u;
        
        Edge *e = g->heads[u];
        while (e != NULL) {
            int v = e->to;
            indegree[v]--;
            if (indegree[v] == 0) {
                stack[++top] = v;
            }
            e = e->next;
        }
    }
    
    return index == g->vertexCount;
}

// 关键路径
void criticalPath(Graph *g) {
    int topo[MAX_VERTICES];
    int ve[MAX_VERTICES];  // 最早发生时间
    int vl[MAX_VERTICES];  // 最晚发生时间
    
    // 复制入度(拓扑排序会修改)
    int tempIndegree[MAX_VERTICES];
    memcpy(tempIndegree, indegree, sizeof(indegree));
    
    // 1. 拓扑排序
    int stack[MAX_VERTICES];
    int top = -1;
    for (int i = 0; i < g->vertexCount; i++) {
        if (tempIndegree[i] == 0) {
            stack[++top] = i;
        }
        ve[i] = 0;
    }
    
    int topoIndex = 0;
    while (top != -1) {
        int u = stack[top--];
        topo[topoIndex++] = u;
        
        Edge *e = g->heads[u];
        while (e != NULL) {
            int v = e->to;
            // 更新ve
            if (ve[u] + e->weight > ve[v]) {
                ve[v] = ve[u] + e->weight;
            }
            tempIndegree[v]--;
            if (tempIndegree[v] == 0) {
                stack[++top] = v;
            }
            e = e->next;
        }
    }
    
    // 2. 计算vl
    int sink = topo[g->vertexCount - 1];
    for (int i = 0; i < g->vertexCount; i++) {
        vl[i] = ve[sink];
    }
    
    for (int i = g->vertexCount - 1; i >= 0; i--) {
        int u = topo[i];
        Edge *e = g->heads[u];
        while (e != NULL) {
            int v = e->to;
            if (vl[v] - e->weight < vl[u]) {
                vl[u] = vl[v] - e->weight;
            }
            e = e->next;
        }
    }
    
    // 3. 输出关键路径
    printf("事件\tve\tvl\t关键事件\n");
    for (int i = 0; i < g->vertexCount; i++) {
        printf("v%d\t%d\t%d\t%s\n", i, ve[i], vl[i], 
               (ve[i] == vl[i]) ? "是" : "否");
    }
    
    printf("\n关键活动:\n");
    for (int i = 0; i < g->vertexCount; i++) {
        Edge *e = g->heads[i];
        while (e != NULL) {
            int j = e->to;
            int ete = ve[i];                    // 活动最早开始
            int lte = vl[j] - e->weight;        // 活动最晚开始
            if (ete == lte) {
                printf("v%d → v%d (权值=%d) 是关键活动\n", i, j, e->weight);
            }
            e = e->next;
        }
    }
    
    printf("\n工程最短工期: %d\n", ve[sink]);
}

int main() {
    Graph g;
    initGraph(&g, 6);
    
    // 构建AOE网(顶点0~5)
    addEdge(&g, 0, 1, 3);
    addEdge(&g, 0, 2, 4);
    addEdge(&g, 1, 3, 5);
    addEdge(&g, 1, 4, 6);
    addEdge(&g, 2, 3, 8);
    addEdge(&g, 2, 4, 7);
    addEdge(&g, 3, 5, 6);
    addEdge(&g, 4, 5, 4);
    
    criticalPath(&g);
    
    return 0;
}

运行结果:

text

复制代码
事件    ve      vl      关键事件
v0      0       0       是
v1      3       4       否
v2      4       4       是
v3      12      12      是
v4      11      12      否
v5      18      18      是

关键活动:
v0 → v2 (权值=4) 是关键活动
v2 → v3 (权值=8) 是关键活动
v3 → v5 (权值=6) 是关键活动

工程最短工期: 18

三、AOV网与AOE网对比

对比项 AOV网 AOE网
顶点 活动 事件
先后关系 活动(带权值)
主要问题 拓扑排序(可行性) 关键路径(最短工期)
权值 有(耗时)
应用 课程安排、编译依赖 项目管理、工程进度

四、关键路径的实际意义

应用 说明
项目管理 找出关键任务,重点关注
工期压缩 压缩非关键活动不缩短工期
资源调配 将资源优先分配给关键活动
风险控制 关键活动延迟会直接影响工期

五、小结

这一篇我们学习了拓扑排序和关键路径:

概念 核心 算法 时间复杂度
拓扑排序 AOV网,入度为0 Kahn算法(栈/队列) O(V+E)
关键路径 AOE网,ve/vl 拓扑序 + 逆拓扑序 O(V+E)

关键公式

  • ve[j] = max(ve[i] + w(i,j))

  • vl[i] = min(vl[j] - w(i,j))

  • 关键活动:ve[i] == vl[j] - w(i,j)

注意:关键路径可能有多条,所有关键活动都必须在压缩范围内才能缩短工期。

下一篇我们讲并查集。


六、思考题

  1. 拓扑排序的结果唯一吗?什么情况下不唯一?

  2. 如何用拓扑排序检测一个有向图是否有环?

  3. 关键路径上的活动如果缩短,工期一定会缩短吗?

  4. 如果AOE网有多条关键路径,压缩某条关键路径上的活动能缩短工期吗?

欢迎在评论区讨论你的答案。

相关推荐
Q741_1472 小时前
每日一题 力扣 1320. 二指输入的的最小距离 动态规划 C++ 题解
c++·算法·leetcode·动态规划
实心儿儿2 小时前
C++ —— C++11(2)
开发语言·c++
加油JIAX2 小时前
C++11特性
c++
wfbcg2 小时前
每日算法练习:LeetCode 76. 最小覆盖子串 ✅
算法·leetcode·职场和发展
Wect2 小时前
LeetCode 149. 直线上最多的点数:题解深度剖析
前端·算法·typescript
qianpeng8972 小时前
运动声源的到达结构仿真
算法
费曼学习法2 小时前
线段树:区间查询的"终极武器",一文看透高效范围统计
算法
itman3012 小时前
Windows环境下编译运行C语言程序的方法及工具选择
c语言·visualstudio·mingw·编译器·windows环境
wayz112 小时前
Day 2:线性回归原理与正则化
算法·机器学习·数据分析·回归·线性回归