算法 - 图的实例 - 拓扑排序与关键路径 (Topological Sort and Critical Path)

    xiaoxiao2025-02-05  124

    算法 - 图的实例 - 拓扑排序与关键路径 (Topological Sort and Critical Path)

    返回分类:全部文章 >> 基础知识

    返回上级:编程基础 - 图 (Graph)

    本文将介绍活动网络的基础知识,并用C++实现拓扑排序(Topological Sort)和关键路径(Critical Path)。

    在查看本文之前,需要一些数据结构和程序语言的基础。

    尤其是“矩阵”、“矩阵的压缩(matrix)”、“图(graph)”等的知识。


    文章目录

    算法 - 图的实例 - 拓扑排序与关键路径 (Topological Sort and Critical Path)1 拓扑排序与关键路径简述 (Introduction)2 拓扑排序 (Topological Sort)3 拓扑排序C++代码 (Topological Sort C++ Code)4 关键路径 (Critical Path)5 关键路径C++代码 (Critical Path C++ Code)5 主函数与测试 (Main Method and Testing)5.1 主函数 (Main Method)4.2 打印结果 (Print Output)


    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 。

    V4 V0 V1 V5 V2 V3

    所以拓扑排序顺序:

    V2,V4,V0,……

    V4,V2或V0, ……


    3 拓扑排序C++代码 (Topological Sort C++ Code)

    // Author: https://blog.csdn.net/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 最迟发生的时间,如果超过这个时间,工程将延误。

    举例说明:

    e0 = 9 e1 = 13 e2 = 15 e3 = 9 e4 = 29 e6 = 6 e5 = 7 e7 = 18 e8 = 6 e9 = 12 v0 v1 v2 v3 v5 v4 v6 v7

    此图已经按拓扑排序编号。

    事件最早活动时间 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​)VE(v6​)VE(v7​)​=v0​+e0​=v0​+e1​=max(v1​+e2​,v2​+e3​)=v3​+e6​=max(v2​+e4​,v3​+e5​)=max(v4​+e7​,v5​+e8​)=v6​+e9​​=0+9=0+13=max(9+15,13+9)=24+6=max(13+29,24+7)=max(30+18,42+6)=48+12​=0=9=13=24=30=42=48=60​

    事件最迟活动时间 VL (Vertex Lastest Time) :

    V L ( v 7 ) = V E ( v 7 ) = 60 V L ( v 6 ) = V E ( v 7 ) − e 9 = 60 − 12 = 48 V L ( v 5 ) = V E ( v 6 ) − e 8 = 48 − 6 = 42 V L ( v 4 ) = V E ( v 6 ) − e 7 = 48 − 18 = 30 V L ( v 3 ) = min ⁡ ( V E ( v 5 ) − e 5 , V E ( v 4 ) − e 6 ) = min ⁡ ( 42 − 7 , 30 − 6 ) = 24 V L ( v 2 ) = min ⁡ ( V E ( v 5 ) − e 4 , V E ( v 3 ) − e 3 ) = min ⁡ ( 42 − 29 , 24 − 9 ) = 13 V L ( v 1 ) = V E ( v 3 ) − e 2 = 24 − 15 = 9 V L ( v 0 ) = min ⁡ ( V E ( v 2 ) − e 1 , V E ( v 1 ) − e 0 ) = min ⁡ ( 13 − 13 , 9 − 9 ) = 0 \begin{array}{rlll} VL(v_7) &= VE(v_7) &&= 60 \\ VL(v_6) &= VE(v_7) - e_9 &= 60 - 12 &= 48 \\ VL(v_5) &= VE(v_6) - e_8 &= 48 - 6 &= 42 \\ VL(v_4) &= VE(v_6) - e_7 &= 48 - 18 &= 30 \\ VL(v_3) &= \min(VE(v_5) - e_5, VE(v_4) - e_6) &= \min(42 - 7, 30 - 6) &= 24 \\ VL(v_2) &= \min(VE(v_5) - e_4, VE(v_3) - e_3) &= \min(42 - 29, 24 - 9) &= 13 \\ VL(v_1) &= VE(v_3) - e_2 &= 24 - 15 &= 9 \\ VL(v_0) &= \min(VE(v_2) - e_1, VE(v_1) - e_0) &= \min(13 - 13, 9 - 9) &= 0 \end{array} VL(v7​)VL(v6​)VL(v5​)VL(v4​)VL(v3​)VL(v2​)VL(v1​)VL(v0​)​=VE(v7​)=VE(v7​)−e9​=VE(v6​)−e8​=VE(v6​)−e7​=min(VE(v5​)−e5​,VE(v4​)−e6​)=min(VE(v5​)−e4​,VE(v3​)−e3​)=VE(v3​)−e2​=min(VE(v2​)−e1​,VE(v1​)−e0​)​=60−12=48−6=48−18=min(42−7,30−6)=min(42−29,24−9)=24−15=min(13−13,9−9)​=60=48=42=30=24=13=9=0​

    活动的最早开始时间 WE (Weight Earliest Time) :

    W E ( e 0 ) = V E ( v 0 ) = 0 W E ( e 1 ) = V E ( v 0 ) = 0 W E ( e 2 ) = V E ( v 1 ) = 9 W E ( e 3 ) = V E ( v 2 ) = 13 W E ( e 4 ) = V E ( v 2 ) = 13 W E ( e 5 ) = V E ( v 3 ) = 24 W E ( e 6 ) = V E ( v 3 ) = 24 W E ( e 7 ) = V E ( v 4 ) = 30 W E ( e 8 ) = V E ( v 5 ) = 42 W E ( e 9 ) = V E ( v 6 ) = 48 \begin{array}{rllrll} WE(e_0) &= VE(v_0) &= 0 \\ WE(e_1) &= VE(v_0) &= 0 \\ WE(e_2) &= VE(v_1) &= 9 \\ WE(e_3) &= VE(v_2) &= 13 \\ WE(e_4) &= VE(v_2) &= 13 \\ WE(e_5) &= VE(v_3) &= 24 \\ WE(e_6) &= VE(v_3) &= 24 \\ WE(e_7) &= VE(v_4) &= 30 \\ WE(e_8) &= VE(v_5) &= 42 \\ WE(e_9) &= VE(v_6) &= 48 \\ \end{array} WE(e0​)WE(e1​)WE(e2​)WE(e3​)WE(e4​)WE(e5​)WE(e6​)WE(e7​)WE(e8​)WE(e9​)​=VE(v0​)=VE(v0​)=VE(v1​)=VE(v2​)=VE(v2​)=VE(v3​)=VE(v3​)=VE(v4​)=VE(v5​)=VE(v6​)​=0=0=9=13=13=24=24=30=42=48​

    活动的最迟开始时间 WL (Weight Lastest Time) :

    W L ( e 0 ) = V L ( v 1 ) − e 0 = 9 − 9 = 0 W L ( e 1 ) = V L ( v 2 ) − e 1 = 13 − 13 = 0 W L ( e 2 ) = V L ( v 3 ) − e 2 = 24 − 15 = 9 W L ( e 3 ) = V L ( v 3 ) − e 3 = 24 − 9 = 15 W L ( e 4 ) = V L ( v 5 ) − e 4 = 42 − 29 = 13 W L ( e 5 ) = V L ( v 5 ) − e 5 = 42 − 7 = 35 W L ( e 6 ) = V L ( v 4 ) − e 6 = 30 − 6 = 24 W L ( e 7 ) = V L ( v 6 ) − e 7 = 48 − 18 = 30 W L ( e 8 ) = V L ( v 6 ) − e 8 = 48 − 6 = 42 W L ( e 9 ) = V L ( v 7 ) − e 9 = 60 − 12 = 48 \begin{array}{rlll} WL(e_0) &= VL(v_1) - e_0 &= 9 - 9 &= 0 \\ WL(e_1) &= VL(v_2) - e_1 &= 13 - 13 &= 0 \\ WL(e_2) &= VL(v_3) - e_2 &= 24 - 15 &= 9 \\ WL(e_3) &= VL(v_3) - e_3 &= 24 - 9 &= 15 \\ WL(e_4) &= VL(v_5) - e_4 &= 42 - 29 &= 13 \\ WL(e_5) &= VL(v_5) - e_5 &= 42 - 7 &= 35 \\ WL(e_6) &= VL(v_4) - e_6 &= 30 - 6 &= 24 \\ WL(e_7) &= VL(v_6) - e_7 &= 48 - 18 &= 30 \\ WL(e_8) &= VL(v_6) - e_8 &= 48 - 6 &= 42 \\ WL(e_9) &= VL(v_7) - e_9 &= 60 - 12 &= 48 \\ \end{array} WL(e0​)WL(e1​)WL(e2​)WL(e3​)WL(e4​)WL(e5​)WL(e6​)WL(e7​)WL(e8​)WL(e9​)​=VL(v1​)−e0​=VL(v2​)−e1​=VL(v3​)−e2​=VL(v3​)−e3​=VL(v5​)−e4​=VL(v5​)−e5​=VL(v4​)−e6​=VL(v6​)−e7​=VL(v6​)−e8​=VL(v7​)−e9​​=9−9=13−13=24−15=24−9=42−29=42−7=30−6=48−18=48−6=60−12​=0=0=9=15=13=35=24=30=42=48​

    我们列成表格对比,

    Activity(Weight)0123456789WE00913132424304248WL00915133524304248

    你会看到,其中表格中 W E ( e 3 ) ≠ W L ( e 3 ) WE(e_3) \neq WL(e_3) WE(e3​)̸​=WL(e3​) 和 W E ( e 5 ) ≠ W L ( e 5 ) WE(e_5) \neq WL(e_5) WE(e5​)̸​=WL(e5​) ,这表明了 e3 和 e5 不是关键活动。

    则我们将 e3 和 e5 删除后,得到两条关键路径:

    { v0, v1, v3, v4, v6, v7 }

    v0 v1 v2 v3 v5 v4 v6 v7

    { v0, v2, v5, v6, v7 }

    v0 v1 v2 v3 v5 v4 v6 v7

    5 关键路径C++代码 (Critical Path C++ Code)

    // Author: https://blog.csdn.net/DarkRabbit // Activity Network struct CriticalEdge { int firstVertex; int secondVertex; double weight; }; // 获取删除边后的矩阵 // params: // graph: 图 // disableEdgesMatrix: 新的矩阵 void GetDisableEdges(AMGraphInt* graph, std::vector<std::vector<double>>& disableEdgesMatrix) { double infinity = graph->GetDefaultWeight(); // 无边权值,即正无穷 int size = graph->GetVertexCount(); // 顶点数量 std::vector<CriticalEdge> edges; disableEdgesMatrix.assign(size, std::vector<double>(size, infinity)); double weight; for (int r = 0; r < size; r++) // Edge { for (int adj = graph->FirstNeighbor(r); adj != -1; adj = graph->NextNeighbor(r, adj)) { CriticalEdge edge; edge.firstVertex = r; edge.secondVertex = adj; graph->TryGetWeight(r, adj, edge.weight); edges.push_back(edge); disableEdgesMatrix[r][adj] = edge.weight; } } std::vector<double> vertexEarliest(size, 0.0); for (int i = 0; i < edges.size(); i++) // VE { vertexEarliest[edges[i].secondVertex] = std::max(vertexEarliest[edges[i].secondVertex], vertexEarliest[edges[i].firstVertex] + edges[i].weight); } std::vector<double> vertexLastest(vertexEarliest); for (int i = 0; i < edges.size(); i++) // VL { vertexLastest[edges[i].firstVertex] = std::min(vertexLastest[edges[i].firstVertex], vertexEarliest[edges[i].secondVertex] - edges[i].weight); } for (int i = 0; i < edges.size(); i++) // WE WL { if (vertexEarliest[edges[i].firstVertex] != vertexLastest[edges[i].secondVertex] - edges[i].weight) { // 不是关键活动,断开边 disableEdgesMatrix[edges[i].firstVertex][edges[i].secondVertex] = infinity; } } } // 获取关键路径的回溯递归 // params: // matrix: 邻接矩阵 // loopStack: 路径 // mark: 顶点标识,如果为真,则有环 // infinity: 无穷符号 // vertex: 当前递归的顶点 // paths: 输出的关键路径 // return: // bool: 是否有环 bool CriticalPathRecursion(const std::vector<std::vector<double>>& matrix, std::stack<int>& loopStack, std::vector<bool>& mark, const double& infinity, int vertex, std::vector<std::stack<int>>& paths) { if (mark[vertex]) { return false; // 再次到达此顶点,证明有环 } mark[vertex] = true; int outdegree = 0; // 记录出度数量,在拓扑排序中,一定有出度为0的顶点 for (int c = 0; c < matrix.size(); c++) { if (matrix[vertex][c] != infinity) { loopStack.push(vertex); if (!CriticalPathRecursion(matrix, loopStack, mark, infinity, c, paths)) { return false; // 有环 } loopStack.pop(); outdegree++; } } mark[vertex] = false; if (outdegree == 0) // 找到没有出度的顶点,加入路径 { loopStack.push(vertex); paths.push_back(loopStack); loopStack.pop(); } return true; } // 关键路径 // params: // graph: 需要排序的图 // paths: 输出的关键路径 // return: // bool: 是否出错 bool CriticalPath(AMGraphInt* graph, std::vector<std::stack<int>>& paths) { if (graph == nullptr || graph->GetVertexCount() == 0) { return false; } std::vector<std::vector<double>> matrix; GetDisableEdges(graph, matrix); double infinity = graph->GetDefaultWeight(); // 无边权值,即正无穷 int size = graph->GetVertexCount(); // 顶点数量 std::stack<int> vertexStack; // 初始顶点栈 std::vector<int> indegrees; // 顶点入度 // 获取首批没有直接前驱的顶点和计算所有入度 if (!GetBeginVertexesAndIndegrees(graph, vertexStack, indegrees)) { return false; // 没有顶点,说明起始图就是个环 } std::stack<int> loopStack; std::vector<bool> marks(size, false); while (!vertexStack.empty()) // 从没有前驱的顶点开始寻找关键路径 { if (!CriticalPathRecursion(matrix, loopStack, marks, infinity, vertexStack.top(), paths)) // 寻找关键路径 { return false; // 有环 } vertexStack.pop(); } return true; }

    5 主函数与测试 (Main Method and Testing)

    5.1 主函数 (Main Method)

    #include "abstract_graph.h" #include "adjacency_matrix.h" #include "minimum_spanning_tree.h" #include "shortest_path.h" #include "activity_network.h" #include <string> #include <vector> #include <stack> #include <unordered_map> #include <iomanip> #include <iostream> using namespace std; typedef Graphs::AMVertexNode<int> AMVertexInt; typedef Graphs::AdjacencyMatrix<int> AMGraphInt; void TestAdjacencyMatrix(); void TestKruskal(); void TestPrim(); void TestDujkstra(); void TestFloyed(); void TestTopologicalSort(); void TestCriticalPath(); int main() { //TestAdjacencyMatrix(); //TestKruskal(); //TestPrim(); //TestDujkstra(); //TestFloyed(); TestTopologicalSort(); TestCriticalPath(); system("pause"); return 0; } // 打印邻接矩阵 template<class TGraph> void PrintMatrix(TGraph& graph); // 拓扑排序 Topological Sort void TestTopologicalSort() { AMGraphInt* graph = new AMGraphInt({ 0, 1, 2, 3, 4, 5 }, true); // 有向图 0-5 graph->InitializeUndirectedWeights( { {0, 1}, {0, 3}, {1, 5}, {2, 1}, {2, 5}, {4, 0}, {4, 1}, {4, 5} }, 1.0); // 初始化边 cout << "----------拓扑排序 Topological Sort----------" << endl; cout << "有向图:" << endl; cout << "邻接矩阵:" << endl; PrintMatrix(*graph); cout << endl; std::vector<int> paths; if (Graphs::ActivityNetwork::TopologicalSort(graph, paths)) { cout << "拓扑排序:"; for (int i = 0; i < paths.size(); i++) { cout << (char)(paths[i] + 'A') << "(V" << paths[i] << ") "; } cout << endl; } else { cout << "拓扑排序失败,有环" << endl; } delete graph; cout << endl; } // 关键路径 Critical Path void TestCriticalPath() { AMGraphInt* graph = new AMGraphInt({ 0, 1, 2, 3, 4, 5, 6, 7 }, true); // 有向图 0-7 graph->InitializeWeights( { {0, 1}, {0, 2}, {1, 3}, {2, 3}, {2, 5}, {3, 5}, {3, 4}, {4, 6}, {5, 6}, {6, 7} }, { 9, 13, 15, 9, 29, 7, 6, 18, 6, 12 }); // 初始化边 cout << "----------关键路径 Critical Path----------" << endl; cout << "有向图:" << endl; cout << "邻接矩阵:" << endl; PrintMatrix(*graph); cout << endl; vector<stack<int>> paths; if (Graphs::ActivityNetwork::CriticalPath(graph, paths)) { string str; string strAlpha; for (int i = 0; i < paths.size(); i++) { str = ""; strAlpha = ""; while (!paths[i].empty()) { str = "v" + to_string(paths[i].top()) + " " + str; strAlpha.insert(strAlpha.begin(), (char)(paths[i].top() + 'A')); paths[i].pop(); } cout << "关键路径 " << (i + 1) << ": " << str; cout << " (" << strAlpha << ")" << endl; } } else { cout << "关键路径失败,有环" << endl; } delete graph; cout << endl; }

    4.2 打印结果 (Print Output)

    ----------拓扑排序 Topological Sort---------- 有向图: 邻接矩阵: A B C D E F A . 1 . 1 . . B . . . . . 1 C . 1 . . . 1 D . . . . . . E 1 1 . . . 1 F . . . . . . 拓扑排序:E(V4) A(V0) D(V3) C(V2) B(V1) F(V5) ----------关键路径 Critical Path---------- 有向图: 邻接矩阵: A B C D E F G H A . 9 13 . . . . . B . . . 15 . . . . C . . . 9 . 29 . . D . . . . 6 7 . . E . . . . . . 18 . F . . . . . . 6 . G . . . . . . . 12 H . . . . . . . . 关键路径 1: v0 v1 v3 v4 v6 v7 (ABDEGH) 关键路径 2: v0 v2 v5 v6 v7 (ACFGH) 请按任意键继续. . .
    最新回复(0)