图 图(Graph) 是一种用于表示关系的非线性数据结构,由顶点(节点,Vertices/Nodes) 和边(Edges) 组成,表示顶点之间的关系。图的概念适用于很多场景,比如社交网络(用户和好友关系)、地图(城市和路线)等。
图的基本概念
顶点(Vertex) :图中的基本单位,表示实体或对象。
边(Edge) :连接两个顶点的线,表示它们之间的关系或路径。
有向图(Directed Graph) :边有方向的图,边表示从一个顶点到另一个顶点的单向关系。
无向图(Undirected Graph) :边没有方向的图,边表示顶点之间的双向关系。
加权图(Weighted Graph) :每条边上都有一个权重或费用,用于表示顶点之间的距离或成本。
邻接 :如果两个顶点之间有直接边相连,则称它们是邻接的。
路径(Path) :从一个顶点到另一个顶点的一条顶点序列,其中相邻顶点之间都有边相连。
环(Cycle) :从一个顶点出发经过若干边回到该顶点的路径。
连通图(Connected Graph) :在无向图中,任何两个顶点之间都可以找到路径的图。
图的存储
邻接矩阵(Adjacency Matrix) :用二维数组表示顶点之间是否相连。
邻接表(Adjacency List) :用链表或数组表示每个顶点相邻的顶点。
以下是使用邻接矩阵和邻接表表示图的简洁 C++ 代码示例。
邻接矩阵法 邻接矩阵使用二维数组表示图,其中 matrix[i][j] = 1
表示顶点 i
和 j
之间有边,0
表示没有边。适合稠密图(边较多)的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> #include <vector> using namespace std;class GraphMatrix {private : vector<vector<int >> matrix; int numVertices; public : GraphMatrix (int vertices) { numVertices = vertices; matrix.resize (vertices, vector <int >(vertices, 0 )); } void addEdge (int src, int dest) { matrix[src][dest] = 1 ; matrix[dest][src] = 1 ; } void display () { for (const auto & row : matrix) { for (int val : row) cout << val << " " ; cout << endl; } } }; int main () { GraphMatrix graph (5 ) ; graph.addEdge (0 , 1 ); graph.addEdge (0 , 2 ); graph.addEdge (1 , 2 ); graph.addEdge (1 , 3 ); graph.display (); return 0 ; }
邻接表法 邻接表使用链表或向量数组来存储图,每个顶点有一个链表(或向量)表示其相邻顶点,适合稀疏图(边较少)的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <vector> using namespace std;class GraphList {private : vector<vector<int >> adjList; int numVertices; public : GraphList (int vertices) { numVertices = vertices; adjList.resize (vertices); } void addEdge (int src, int dest) { adjList[src].push_back (dest); adjList[dest].push_back (src); } void display () { for (int i = 0 ; i < numVertices; ++i) { cout << i << " -> " ; for (int neighbor : adjList[i]) cout << neighbor << " " ; cout << endl; } } }; int main () { GraphList graph (5 ) ; graph.addEdge (0 , 1 ); graph.addEdge (0 , 2 ); graph.addEdge (1 , 2 ); graph.addEdge (1 , 3 ); graph.display (); return 0 ; }
说明
邻接矩阵 :适合用于边较多的图,空间复杂度为 (O(V^2))。
邻接表 :适合用于边较少的图,空间复杂度为 (O(V + E)),更节省空间。
图的遍历操作 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <iostream> #include <vector> #include <queue> using namespace std;void addEdge (vector<vector<int >>& adjList, int src, int dest) { adjList[src].push_back (dest); adjList[dest].push_back (src); } void DFS (int start, vector<vector<int >>& adjList, vector<bool >& visited) { visited[start] = true ; cout << start << " " ; for (int neighbor : adjList[start]) { if (!visited[neighbor]) { DFS (neighbor, adjList, visited); } } } void BFS (int start, vector<vector<int >>& adjList, vector<bool >& visited) { queue<int > q; visited[start] = true ; q.push (start); while (!q.empty ()) { int v = q.front (); q.pop (); cout << v << " " ; for (int neighbor : adjList[v]) { if (!visited[neighbor]) { visited[neighbor] = true ; q.push (neighbor); } } } } int main () { int numVertices = 5 ; vector<vector<int >> adjList (numVertices); addEdge (adjList, 0 , 1 ); addEdge (adjList, 0 , 2 ); addEdge (adjList, 1 , 2 ); addEdge (adjList, 1 , 3 ); addEdge (adjList, 3 , 4 ); cout << "DFS starting from vertex 0: " ; vector<bool > visited (numVertices, false ) ; DFS (0 , adjList, visited); cout << endl; cout << "BFS starting from vertex 0: " ; fill (visited.begin (), visited.end (), false ); BFS (0 , adjList, visited); cout << endl; return 0 ; }
代码说明:
邻接表的创建 :用二维 vector
表示邻接表,其中 adjList[i]
存储顶点 i
的所有相邻顶点。
addEdge
:在邻接表中添加一条边。由于是无向图,所以需要添加双向连接。
DFS :递归遍历所有相邻的未访问顶点,打印访问顺序。
BFS :使用队列逐层遍历,打印访问顺序。
visited
:在 DFS 和 BFS 中用于标记访问过的顶点。
输出示例 1 2 DFS starting from vertex 0: 0 1 2 3 4 BFS starting from vertex 0: 0 1 2 3 4
DFS 先沿着一条路径深入,再回溯。
BFS 逐层遍历,适合用于无权图中的最短路径查找。
最小生成树 最小生成树(Minimum Spanning Tree,MST)。它用于找到一个无向加权连通图中连接所有顶点的最小代价子图。生成树的边数为 (V - 1)(其中 (V) 是顶点数),且没有环。生成树的总权重最小,即最小生成树。
最小生成树的两个主要算法
Kruskal 算法 :
基于“贪心算法”思想。
按权重从小到大排序所有边,并按顺序选择最小权重的边,只要它不形成环。
常用“并查集”来检测环。
时间复杂度 :(O(E \log E)),其中 (E) 是边数。
Prim 算法 :
也是基于“贪心算法”。
从任意一个顶点开始,将该顶点加入 MST,之后重复选择与已加入 MST 的顶点相邻的权重最小的边。
常用优先队列(最小堆)来优化边的选择。
时间复杂度 :(O(E \log V))。
Kruskal 算法的实现(C++ 代码示例) Kruskal 算法适合边较少的稀疏图,因为它的效率主要取决于边的数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <iostream> #include <vector> #include <algorithm> using namespace std;struct Edge { int src, dest, weight; bool operator <(const Edge& other) const { return weight < other.weight; } }; class UnionFind {private : vector<int > parent, rank; public : UnionFind (int n) { parent.resize (n); rank.resize (n, 0 ); for (int i = 0 ; i < n; ++i) { parent[i] = i; } } int find (int u) { if (parent[u] != u) parent[u] = find (parent[u]); return parent[u]; } bool unionSets (int u, int v) { int rootU = find (u); int rootV = find (v); if (rootU == rootV) return false ; if (rank[rootU] > rank[rootV]) { parent[rootV] = rootU; } else if (rank[rootU] < rank[rootV]) { parent[rootU] = rootV; } else { parent[rootV] = rootU; ++rank[rootU]; } return true ; } }; int kruskalMST (int V, vector<Edge>& edges) { sort (edges.begin (), edges.end ()); UnionFind uf (V) ; int mstWeight = 0 ; for (const Edge& edge : edges) { if (uf.unionSets (edge.src, edge.dest)) { mstWeight += edge.weight; cout << "Edge (" << edge.src << " - " << edge.dest << ") with weight " << edge.weight << " added to MST\n" ; } } return mstWeight; } int main () { int V = 4 ; vector<Edge> edges = { {0 , 1 , 10 }, {0 , 2 , 6 }, {0 , 3 , 5 }, {1 , 3 , 15 }, {2 , 3 , 4 } }; cout << "Minimum Spanning Tree Weight: " << kruskalMST (V, edges) << endl; return 0 ; }
Prim 算法的实现(C++ 代码示例) Prim 算法适合边较多的稠密图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <vector> #include <queue> using namespace std;typedef pair<int , int > pii; int primMST (int V, vector<vector<pii>>& adjList) { vector<bool > inMST (V, false ) ; priority_queue<pii, vector<pii>, greater<pii>> pq; int mstWeight = 0 ; pq.push ({0 , 0 }); while (!pq.empty ()) { auto [weight, u] = pq.top (); pq.pop (); if (inMST[u]) continue ; inMST[u] = true ; mstWeight += weight; cout << "Vertex " << u << " added to MST with edge weight " << weight << endl; for (auto [neighborWeight, v] : adjList[u]) { if (!inMST[v]) { pq.push ({neighborWeight, v}); } } } return mstWeight; } int main () { int V = 4 ; vector<vector<pii>> adjList (V); adjList[0 ].push_back ({10 , 1 }); adjList[0 ].push_back ({6 , 2 }); adjList[0 ].push_back ({5 , 3 }); adjList[1 ].push_back ({10 , 0 }); adjList[1 ].push_back ({15 , 3 }); adjList[2 ].push_back ({6 , 0 }); adjList[2 ].push_back ({4 , 3 }); adjList[3 ].push_back ({5 , 0 }); adjList[3 ].push_back ({15 , 1 }); adjList[3 ].push_back ({4 , 2 }); cout << "Minimum Spanning Tree Weight: " << primMST (V, adjList) << endl; return 0 ; }
代码解释
Kruskal 算法 :
使用 UnionFind
数据结构来管理连接和检测环。
按权重排序边,并依次检查,若边不会形成环,则将其加入 MST。
Prim 算法 :
从一个起始顶点出发,使用优先队列(最小堆)记录与已加入顶点相邻的最小边。
每次从优先队列中取出权重最小的边,将其对应的顶点加入 MST。
输出示例 1 2 3 4 Edge (2 - 3) with weight 4 added to MST Edge (0 - 3) with weight 5 added to MST Edge (0 - 1) with weight 10 added to MST Minimum Spanning Tree Weight: 19
总结
Kruskal 算法 :适合边较少的图,按边排序后添加到 MST。
Prim 算法 :适合边较多的图,优先从已连接的顶点选择最小的边。
最短路径 最短路径算法用于在加权图中查找从起点到终点的最短距离(权重和最小)路径。
最短路径算法的主要方法 Dijkstra 算法 :
适用于边权非负的图。
使用“贪心算法”思想,每次选择距离起点最近的顶点并更新它的邻接顶点。
利用优先队列(最小堆)实现效率更高的版本。
时间复杂度 :(O(E \log V)),其中 (E) 是边数,(V) 是顶点数。
Dijkstra 算法的实现(C++ 代码示例) Dijkstra 算法适用于边权为非负的图,特别适合用于单源最短路径问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <iostream> #include <vector> #include <queue> #include <limits> using namespace std;typedef pair<int , int > pii; vector<int > dijkstra (int V, vector<vector<pii>>& adjList, int start) { vector<int > dist (V, numeric_limits<int >::max()) ; priority_queue<pii, vector<pii>, greater<pii>> pq; dist[start] = 0 ; pq.push ({0 , start}); while (!pq.empty ()) { int u = pq.top ().second; int d = pq.top ().first; pq.pop (); if (d > dist[u]) continue ; for (auto [weight, v] : adjList[u]) { if (dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; pq.push ({dist[v], v}); } } } return dist; } int main () { int V = 5 ; vector<vector<pii>> adjList (V); adjList[0 ].push_back ({10 , 1 }); adjList[0 ].push_back ({3 , 2 }); adjList[1 ].push_back ({1 , 2 }); adjList[1 ].push_back ({2 , 3 }); adjList[2 ].push_back ({8 , 1 }); adjList[2 ].push_back ({2 , 3 }); adjList[2 ].push_back ({5 , 4 }); adjList[3 ].push_back ({4 , 4 }); int start = 0 ; vector<int > distances = dijkstra (V, adjList, start); cout << "Shortest distances from vertex " << start << ":\n" ; for (int i = 0 ; i < V; i++) { cout << "Vertex " << i << ": " << distances[i] << endl; } return 0 ; }
代码解释
使用最小堆实现贪心选择每次最短路径的顶点。
每次从堆中提取距离最小的顶点,检查其邻接顶点并更新距离。
时间复杂度 (O(E \log V)) 主要取决于堆操作。
SPFA(Shortest Path Faster Algorithm)算法
原理 :SPFA 算法是 Bellman-Ford 的改进版,利用队列来加速松弛过程。SPFA 仅对需要更新的节点进行处理,不必在每一轮松弛时遍历所有边。
适用场景 :通常在大部分边为正、且负权边较少的图中,SPFA 表现非常好。
时间复杂度 :最坏情况下为 (O(V \times E)),但在实际应用中通常远优于 Bellman-Ford。平均复杂度接近 (O(E))。
SPFA 算法实现示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <iostream> #include <vector> #include <queue> #include <limits> using namespace std;struct Edge { int dest, weight; }; bool SPFA (int V, vector<vector<Edge>>& adjList, int start, vector<int >& dist) { vector<bool > inQueue (V, false ) ; vector<int > count (V, 0 ) ; dist.assign (V, numeric_limits<int >::max ()); dist[start] = 0 ; queue<int > q; q.push (start); inQueue[start] = true ; while (!q.empty ()) { int u = q.front (); q.pop (); inQueue[u] = false ; for (auto & edge : adjList[u]) { int v = edge.dest; int weight = edge.weight; if (dist[u] != numeric_limits<int >::max () && dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; if (!inQueue[v]) { q.push (v); inQueue[v] = true ; count[v]++; if (count[v] > V - 1 ) { return false ; } } } } } return true ; } int main () { int V = 5 ; vector<vector<Edge>> adjList (V); adjList[0 ].push_back ({1 , 10 }); adjList[0 ].push_back ({2 , 3 }); adjList[1 ].push_back ({2 , 1 }); adjList[1 ].push_back ({3 , 2 }); adjList[2 ].push_back ({1 , 8 }); adjList[2 ].push_back ({3 , 2 }); adjList[2 ].push_back ({4 , 5 }); adjList[3 ].push_back ({4 , 4 }); vector<int > dist; int start = 0 ; if (SPFA (V, adjList, start, dist)) { cout << "Shortest distances from vertex " << start << ":\n" ; for (int i = 0 ; i < V; i++) { cout << "Vertex " << i << ": " << dist[i] << endl; } } else { cout << "Graph contains a negative weight cycle" << endl; } return 0 ; }
拓扑排序 拓扑排序(Topological Sorting)用于有向无环图(DAG)中,对图中的节点进行线性排序,以满足每个有向边 ( u \rightarrow v ) 中节点 ( u ) 必须排在节点 ( v ) 之前的要求。拓扑排序常用于解决依赖关系问题,比如任务调度、编译依赖等。
拓扑排序的基本原理
前提条件 :
拓扑排序只能在 有向无环图(DAG) 上实现,因为如果有环存在,就无法对节点进行线性排序。
拓扑排序方法 :
拓扑排序的实现通常有两种方法:DFS 和 Kahn 算法(BFS) 。
主要步骤 :
DFS法 :对每个未访问的节点进行 DFS 遍历,当遍历到没有后续节点时,将该节点记录下来。这样会得到一个逆序的排序结果。
Kahn 算法(BFS) :利用入度的概念,将入度为 0 的节点依次加入队列,处理该节点后将其指向的节点入度减一,并将新入度为 0 的节点加入队列,直至队列为空。
拓扑排序的两种实现 方法一:DFS 法 DFS 法适合用递归的方式实现。
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <vector> #include <stack> using namespace std;void topologicalSortDFS (int v, vector<vector<int >>& adjList, vector<bool >& visited, stack<int >& topoStack) { visited[v] = true ; for (int neighbor : adjList[v]) { if (!visited[neighbor]) { topologicalSortDFS (neighbor, adjList, visited, topoStack); } } topoStack.push (v); } void topologicalSort (int V, vector<vector<int >>& adjList) { vector<bool > visited (V, false ) ; stack<int > topoStack; for (int i = 0 ; i < V; i++) { if (!visited[i]) { topologicalSortDFS (i, adjList, visited, topoStack); } } cout << "Topological Sort (DFS): " ; while (!topoStack.empty ()) { cout << topoStack.top () << " " ; topoStack.pop (); } cout << endl; } int main () { int V = 6 ; vector<vector<int >> adjList (V); adjList[5 ].push_back (2 ); adjList[5 ].push_back (0 ); adjList[4 ].push_back (0 ); adjList[4 ].push_back (1 ); adjList[2 ].push_back (3 ); adjList[3 ].push_back (1 ); topologicalSort (V, adjList); return 0 ; }
输出示例 :
1 Topological Sort (DFS): 5 4 2 3 1 0
方法二:Kahn 算法(BFS 法) Kahn 算法通过入度(in-degree)来实现拓扑排序:
计算每个节点的入度,将入度为 0 的节点加入队列。
依次从队列中取出节点,输出其编号。
将该节点指向的所有节点的入度减 1,如果入度变为 0,则将该节点加入队列。
直到队列为空,最终的输出顺序即为拓扑排序结果。
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <iostream> #include <vector> #include <queue> using namespace std;void topologicalSortKahn (int V, vector<vector<int >>& adjList) { vector<int > inDegree (V, 0 ) ; for (int i = 0 ; i < V; i++) { for (int neighbor : adjList[i]) { inDegree[neighbor]++; } } queue<int > q; for (int i = 0 ; i < V; i++) { if (inDegree[i] == 0 ) { q.push (i); } } cout << "Topological Sort (Kahn's Algorithm): " ; while (!q.empty ()) { int u = q.front (); q.pop (); cout << u << " " ; for (int neighbor : adjList[u]) { inDegree[neighbor]--; if (inDegree[neighbor] == 0 ) { q.push (neighbor); } } } cout << endl; } int main () { int V = 6 ; vector<vector<int >> adjList (V); adjList[5 ].push_back (2 ); adjList[5 ].push_back (0 ); adjList[4 ].push_back (0 ); adjList[4 ].push_back (1 ); adjList[2 ].push_back (3 ); adjList[3 ].push_back (1 ); topologicalSortKahn (V, adjList); return 0 ; }
输出示例 :
1 Topological Sort (Kahn's Algorithm): 4 5 0 2 3 1
拓扑排序方法总结
方法
主要步骤
复杂度
优缺点
DFS 法
DFS 遍历节点,将节点按逆序入栈
(O(V + E))
适合递归实现,空间开销较少
Kahn 算法(BFS 法)
计算入度,队列处理入度为 0 的节点
(O(V + E))
不适合递归实现,但逻辑较直观
注意事项
有环图无法拓扑排序 :如果在 Kahn 算法中,无法输出所有节点,则说明存在环;在 DFS 法中若检测到回到已访问节点则说明有环。
关键路径 关键路径算法(Critical Path Method, CPM)主要用于 有向无环图(DAG) 中的 任务调度问题 ,帮助确定任务的最长路径(即关键路径)以及项目的最短完成时间。关键路径中的任务没有时间余地,一旦延迟会导致项目整体延误。
关键路径的基本原理
事件的最早开始时间(earliest start, ES) :
从起点向终点计算,在满足所有前序任务完成的情况下,每个任务可以开始的最早时间。
事件的最晚开始时间(latest start, LS) :
从终点向起点反向计算,在不延迟项目总工期的前提下,每个任务可以推迟到的最晚开始时间。
计算方法 :
通过 正向遍历 和 反向遍历 ,分别计算每个任务的最早和最晚完成时间。
关键路径 是从起点到终点的最长路径(即时间最长的路径),这条路径上的所有任务都为关键任务,任何一个任务的延迟都会影响整个项目的工期。
关键路径的实现(C++ 代码) 假设任务的关系以有向无环图的邻接表形式给出,图中每个节点表示一个任务,边表示任务依赖,边的权重表示任务所需时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <iostream> #include <vector> #include <stack> #include <algorithm> #include <limits> using namespace std;struct Edge { int dest, weight; }; void topologicalSort (int v, vector<vector<Edge>>& adjList, vector<bool >& visited, stack<int >& topoStack) { visited[v] = true ; for (const auto & edge : adjList[v]) { if (!visited[edge.dest]) { topologicalSort (edge.dest, adjList, visited, topoStack); } } topoStack.push (v); } int criticalPath (int V, vector<vector<Edge>>& adjList) { vector<int > earliest (V, 0 ) ; vector<int > latest (V, numeric_limits<int >::max()) ; vector<bool > visited (V, false ) ; stack<int > topoStack; for (int i = 0 ; i < V; i++) { if (!visited[i]) { topologicalSort (i, adjList, visited, topoStack); } } while (!topoStack.empty ()) { int u = topoStack.top (); topoStack.pop (); for (const auto & edge : adjList[u]) { int v = edge.dest; earliest[v] = max (earliest[v], earliest[u] + edge.weight); } } int projectDuration = *max_element (earliest.begin (), earliest.end ()); fill (latest.begin (), latest.end (), projectDuration); for (int u = V - 1 ; u >= 0 ; u--) { for (const auto & edge : adjList[u]) { int v = edge.dest; latest[u] = min (latest[u], latest[v] - edge.weight); } } cout << "Critical Path: " ; for (int i = 0 ; i < V; i++) { for (const auto & edge : adjList[i]) { int j = edge.dest; if (earliest[i] == latest[i] && earliest[i] + edge.weight == earliest[j]) { cout << i << " -> " ; } } } cout << "End" << endl; return projectDuration; } int main () { int V = 6 ; vector<vector<Edge>> adjList (V); adjList[0 ].push_back ({1 , 3 }); adjList[0 ].push_back ({2 , 2 }); adjList[1 ].push_back ({3 , 2 }); adjList[1 ].push_back ({4 , 4 }); adjList[2 ].push_back ({4 , 2 }); adjList[3 ].push_back ({5 , 3 }); adjList[4 ].push_back ({5 , 2 }); int projectDuration = criticalPath (V, adjList); cout << "Project Duration: " << projectDuration << endl; return 0 ; }
代码解释
拓扑排序 :
由于关键路径算法应用于有向无环图(DAG),首先对图进行拓扑排序,便于按依赖顺序处理任务。
topologicalSort
函数使用递归 DFS 实现拓扑排序。
正向遍历计算最早开始时间 :
从拓扑排序后的起点开始,根据前序任务的最早完成时间计算每个任务的最早开始时间。
反向遍历计算最晚开始时间 :
从终点倒着计算,尽量推迟任务时间(保证不影响总工期),计算每个任务的最晚开始时间。
使用 fill
将 latest
数组初始化为总工期,这样可以方便地更新最晚开始时间。
关键路径 :
关键路径上的任务满足 earliest[i] == latest[i]
,即任务的最早开始时间等于最晚开始时间。通过检查每条边的权重和关键任务条件,确定关键路径并输出。
输出示例 1 2 Critical Path: 0 -> 1 -> 3 -> 5 -> End Project Duration: 8
总结
关键路径 是整个项目的最长路径。沿关键路径的所有任务必须按时完成,以免延误项目。
关键路径算法利用 拓扑排序 来确保按依赖关系遍历任务。