算法 - 图的实例 - 拓扑排序与关键路径 (Topological Sort and Critical Path)
本文将介绍活动网络的基础知识,并用C++实现拓扑排序(Topological Sort)和关键路径(Critical Path)。
在查看本文之前,需要一些数据结构和程序语言的基础。
尤其是“矩阵”、“矩阵的压缩(matrix)”、“图(graph)”等的知识。
文章目录
1 拓扑排序与关键路径简述 (Introduction)
-
AOV网络(Activity on Vertices):用有向图表示一个工程,每一个顶点表示活动,用边表示活动方向,边开始的顶点是结束顶点的前置条件。
- AOV网络不能有有向回路,即不能有有向环;
- 将各个顶点排列成一个线性有序序列的运算,称为拓扑排序;
- 如果网络存在有向环,则表示此工程不可行。
-
AOE网络(Activity on Edges):用有向图表示一个工程,每一条边表示活动,用边上权值表示活动时间,顶点表示事件。
- AOE网络不能有有向回路,即不能有有向环;
- 完成整个工程所需的时间取决于从源点到汇点的最长路径长度,这条最长路径称为关键路径;
- 关键路径上的活动称为关键活动。
2 拓扑排序 (Topological Sort)
拓扑排序方法:
-
(1)输入AOV网络;
-
(2)从AOV网路中选择一个没有直接前驱的顶点,输出;
-
(3)从图中删除该点,同时删除它所有的边;
-
(4)重复步骤(2)和(3),直到所有顶点均输出;或者还剩下顶点,表明此图存在有向环。
例如下图:没有前驱的顶点,只能是 V2 和 V4 。
所以拓扑排序顺序:
-
V2,V4,V0,……
-
V4,V2或V0, ……
3 拓扑排序C++代码 (Topological Sort C++ Code)
// Author: https://blog.youkuaiyun.com/DarkRabbit
// Activity Network
// 获取首批没有直接前驱的顶点和计算所有入度
// params:
// graph: 图
// vertexStack: 起始顶点栈
// indegrees: 输出的入度
// return:
// bool: 是否有起始点,图是否不是环
bool GetBeginVertexesAndIndegrees(AMGraphInt* graph,
std::stack<int>& vertexStack,
std::vector<int>& indegrees)
{
double infinity = graph->GetDefaultWeight(); // 无边权值,即正无穷
int size = graph->GetVertexCount(); // 顶点数量
indegrees.assign(size, 0);
double weight;
for (int c = 0; c < size; c++) // 对列循环,即终点
{
bool hasEdge = false;
for (int r = 0; r < size; r++) // 对行循环,即起始点
{
graph->TryGetWeight(r, c, weight); // 获取权重
if (weight != infinity) // 如果有边
{
hasEdge = true;
indegrees[c]++; // 入度+1
}
}
if (!hasEdge) // 如果顶点没有直接前驱
{
vertexStack.push(c); // 加入起始顶点
}
}
return !vertexStack.empty(); // 没有起始点,说明图是个环
}
// 拓扑排序
// params:
// graph: 需要排序的图
// paths: 输出的顺序
// return:
// bool: 是否出错
bool TopologicalSort(AMGraphInt* graph,
std::vector<int>& paths)
{
if (graph == nullptr || !graph->IsOriented()) // 无向图返回
{
return false;
}
paths.clear();
int size = graph->GetVertexCount(); // 顶点数量
if (size == 0) // 没有顶点
{
return true;
}
double infinity = graph->GetDefaultWeight(); // 无边权值,即正无穷
std::stack<int> vertexStack; // 顶点栈
std::vector<int> indegrees; // 顶点入度
// 获取首批没有直接前驱的顶点和计算所有入度
if (!GetBeginVertexesAndIndegrees(graph, vertexStack, indegrees))
{
return false; // 没有顶点,说明起始图就是个环
}
double weight;
for (int i = 0; i < size; i++)
{
if (vertexStack.empty()) // 没有入度为0的顶点了
{
return false; // 有环
}
else
{
int vertex = vertexStack.top();
vertexStack.pop();
paths.push_back(vertex); // 输出路径
// 将此顶点连接的顶点入度-1
for (int c = 0; c < size; c++)
{
graph->TryGetWeight(vertex, c, weight);
// 入度-1,如果没有入度了入栈
if (weight != infinity && --indegrees[c] == 0)
{
vertexStack.push(c);
}
}
}
}
return true;
}
4 关键路径 (Critical Path)
关键路径:从源点到汇点具有最大长度的路径;
关键活动:关键路径上的活动;
我们假设带权有向图中,顶点为 { v0, v1, …, vi, …, vn-1 } ,边为 { e0, e1, …, ej,… } 。
-
事件最早开始时间:顶点 vi 最早发生的时间;
-
事件最迟开始时间:顶点 vi 最迟发生的时间,如果超过这个时间,工程将延误;
-
活动的最早开始时间:边 ej 最早发生的时间;
-
活动的最迟开始时间:边 ej 最迟发生的时间,如果超过这个时间,工程将延误。
举例说明:
此图已经按拓扑排序编号。
-
事件最早活动时间 VE (Vertex Earliest Time) :
V E ( v 0 ) = 0 V E ( v 1 ) = v 0 + e 0 = 0 + 9 = 9 V E ( v 2 ) = v 0 + e 1 = 0 + 13 = 13 V E ( v 3 ) = max ( v 1 + e 2 , v 2 + e 3 ) = max ( 9 + 15 , 13 + 9 ) = 24 V E ( v 4 ) = v 3 + e 6 = 24 + 6 = 30 V E ( v 5 ) = max ( v 2 + e 4 , v 3 + e 5 ) = max ( 13 + 29 , 24 + 7 ) = 42 V E ( v 6 ) = max ( v 4 + e 7 , v 5 + e 8 ) = max ( 30 + 18 , 42 + 6 ) = 48 V E ( v 7 ) = v 6 + e 9 = 48 + 12 = 60 \begin{array}{rlll} VE(v_0) &&&= 0 \\ VE(v_1) &= v_0 + e_0 &= 0 + 9 &= 9 \\ VE(v_2) &= v_0 + e_1 &= 0 + 13 &= 13 \\ VE(v_3) &= \max(v_1 + e_2, v_2 + e_3) &= \max(9 + 15, 13 + 9) &= 24 \\ VE(v_4) &= v_3 + e_6 &= 24 + 6 &= 30 \\ VE(v_5) &= \max(v_2 + e_4, v_3 + e_5) &= \max(13 + 29, 24 + 7) &= 42 \\ VE(v_6) &= \max(v_4 + e_7, v_5 + e_8) &= \max(30 + 18, 42 + 6) &= 48 \\ VE(v_7) &= v_6 + e_9 &= 48 + 12 &= 60 \end{array} VE(v0)VE(v1)VE(v2)VE(v3)VE(v4)VE(v5)