目录
[1.1 AOV网的定义](#1.1 AOV网的定义)
[1.2 拓扑排序](#1.2 拓扑排序)
[1.3 算法思想(Kahn算法)](#1.3 算法思想(Kahn算法))
[1.4 代码实现](#1.4 代码实现)
[2.1 AOE网的定义](#2.1 AOE网的定义)
[2.2 关键路径概念](#2.2 关键路径概念)
[2.3 算法步骤](#2.3 算法步骤)
[2.4 代码实现](#2.4 代码实现)
一、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算法)
-
计算所有顶点的入度
-
将所有入度为0的顶点入栈(或队列)
-
弹出顶点,加入拓扑序列,将其所有邻接点的入度减1
-
若邻接点入度变为0,入栈
-
重复直到栈空
判断是否有环:如果输出的顶点数 < 总顶点数,说明图中有环。
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 算法步骤
-
拓扑排序:得到顶点顺序
-
计算ve :按拓扑序,
ve[j] = max(ve[i] + w(i,j)) -
计算vl :按逆拓扑序,
vl[i] = min(vl[j] - w(i,j)),汇点vl=ve -
判断关键活动 :对边
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)
注意:关键路径可能有多条,所有关键活动都必须在压缩范围内才能缩短工期。
下一篇我们讲并查集。
六、思考题
-
拓扑排序的结果唯一吗?什么情况下不唯一?
-
如何用拓扑排序检测一个有向图是否有环?
-
关键路径上的活动如果缩短,工期一定会缩短吗?
-
如果AOE网有多条关键路径,压缩某条关键路径上的活动能缩短工期吗?
欢迎在评论区讨论你的答案。