返回分类:全部文章 >> 基础知识
返回上级:编程基础 - 图 (Graph)
本文将介绍活动网络的基础知识,并用C++实现拓扑排序(Topological Sort)和关键路径(Critical Path)。
在查看本文之前,需要一些数据结构和程序语言的基础。
尤其是“矩阵”、“矩阵的压缩(matrix)”、“图(graph)”等的知识。
AOV网络(Activity on Vertices):用有向图表示一个工程,每一个顶点表示活动,用边表示活动方向,边开始的顶点是结束顶点的前置条件。
AOE网络(Activity on Edges):用有向图表示一个工程,每一条边表示活动,用边上权值表示活动时间,顶点表示事件。
拓扑排序方法:
(1)输入AOV网络;
(2)从AOV网路中选择一个没有直接前驱的顶点,输出;
(3)从图中删除该点,同时删除它所有的边;
(4)重复步骤(2)和(3),直到所有顶点均输出;或者还剩下顶点,表明此图存在有向环。
例如下图:没有前驱的顶点,只能是 V2 和 V4 。
所以拓扑排序顺序:
V2,V4,V0,……
V4,V2或V0, ……
// 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;
}
关键路径:从源点到汇点具有最大长度的路径;
关键活动:关键路径上的活动;
我们假设带权有向图中,顶点为 { 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)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) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
WE | 0 | 0 | 9 | 13 | 13 | 24 | 24 | 30 | 42 | 48 |
WL | 0 | 0 | 9 | 15 | 13 | 35 | 24 | 30 | 42 | 48 |
你会看到,其中表格中 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, v2, v5, v6, v7 }
// 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;
}
#include "abstract_graph.h"
#include "adjacency_matrix.h"
#include "minimum_spanning_tree.h"
#include "shortest_path.h"
#include "activity_network.h"
#include
#include
#include
#include
#include
#include
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;
}
----------拓扑排序 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)
请按任意键继续. . .