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

    xiaoxiao2025-02-05  50

    算法 - 图的实例 - 拓扑排序与关键路径 (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)=6012=486=4818=min(427,306)=min(4229,249)=2415=min(1313,99)=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=99=1313=2415=249=4229=427=306=4818=486=6012=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)