10.关键路径
实验10 关键路径
实验目的
- 通过上机掌握图的存储定义、生成、显示
- 掌握图的拓扑排序与关键路径算法
实验内容
- 从文件中读入有向图(网)并以邻接表存储
- 利用 AOE 网络求取其关键路径
代码
tu.txt (教程P179 图6.28)
9 11
a b c d e f g h i
a b 6
a c 4
a d 5
b e 1
c e 1
d f 2
e g 9
e h 7
f h 4
g i 2
h i 4
critical_path.cpp
#include <algorithm>
#include <iostream>
#include <fstream>
using namespace std;
#define MVNum 20 //最大顶点数
#define GRAPH_FILE "./tu.txt" // 输入的文件路径
struct ArcNode
{
int adjvex; //该边所指向的顶点的位置
int weight; //权值
ArcNode *nextarc; //指向下一条边的指针
};
using VerTexType = char;
struct VNode
{
VerTexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的边的指针
};
using AdjList = VNode[MVNum]; // 邻接表类型
struct ALGraph
{
AdjList vertices; //邻接表
AdjList inverse_vertices; //逆邻接表
int vexnum, arcnum; //图的当前顶点数和边数
};
// 顺序栈的定义
struct spStack
{
int *base;
int *top;
int stacksize;
};
// 栈的初始化
void InitStack(spStack &S)
{
S.base = new int[MVNum];
if (!S.base)
exit(-1);
S.top = S.base;
S.stacksize = MVNum;
}
// 销毁栈
void DestroyStack(spStack &S)
{
delete[] S.base;
}
// 判断栈是否为空
bool StackEmpty(spStack &S)
{
return S.top == S.base;
}
// 元素入栈
void Push(spStack &S, int i)
{
if (S.top - S.base == S.stacksize)
return; // 栈满
*S.top = i;
S.top++; // top 指向下一个空位置
}
// 栈顶元素出栈
void Pop(spStack &S, int &i)
{
if (StackEmpty(S))
return; // 栈空
S.top--;
i = *S.top;
}
//---------------------------------------
// 确定结点 v 在图 G 邻接表的下标
int LocateVex(ALGraph &G, VerTexType v)
{
for (int i = 0; i < G.vexnum; ++i)
if (G.vertices[i].data == v)
return i;
return -1;
}
// 创建有向图G的邻接表、逆邻接表
void CreateUDG(ALGraph &G)
{
fstream file(GRAPH_FILE);
if (!file)
{
cout << "未找到相关文件,无法打开!" << endl;
exit(-1);
}
file >> G.vexnum >> G.arcnum; // 输入结点数, 边数
for (int i = 0; i < G.vexnum; ++i) //输入各点,构造表头结点表
{
file >> G.vertices[i].data; // 输入顶点值
G.inverse_vertices[i].data = G.vertices[i].data;
// 初始化表头结点的指针域为空
G.vertices[i].firstarc = nullptr;
G.inverse_vertices[i].firstarc = nullptr;
}
//输入各边,构造邻接表(头插法)
for (int k = 0; k < G.arcnum; ++k)
{
VerTexType v1, v2;
int weight;
file >> v1 >> v2 >> weight; //输入一条边依附的两个顶点, 和边的权值
int src = LocateVex(G, v1); //确定 v1 在 G 中位置, src 是边的来源结点
int dest = LocateVex(G, v2); //确定 v2 在 G 中位置, dest 是边指向的结点
// 在邻接表插入新的边结点
ArcNode *p1 = new ArcNode;
p1->weight = weight;
p1->adjvex = dest;
p1->nextarc = G.vertices[src].firstarc; // 头插法
G.vertices[src].firstarc = p1;
// 在逆邻接表插入新的边结点
ArcNode *p2 = new ArcNode;
p2->weight = weight;
p2->adjvex = src;
p2->nextarc = G.inverse_vertices[dest].firstarc;
G.inverse_vertices[dest].firstarc = p2;
}
file.close();
}
// 通过逆邻接表求出各顶点的入度存入数组indegree中
void FindInDegree(ALGraph &G, int indegree[])
{
for (int i = 0; i < G.vexnum; i++)
{
int count = 0;
for (ArcNode *p = G.inverse_vertices[i].firstarc; p; p = p->nextarc)
count++;
indegree[i] = count;
}
}
// 求拓扑排序数组, 保存在 topo 中
bool TopologicalOrder(ALGraph &G, int topo[])
{
// 求出各顶点的入度存入数组 indegree 中
int *indegree = new int[G.vexnum];
FindInDegree(G, indegree);
spStack S;
InitStack(S);
// 入度为 0 的结点下标进栈
for (int i = 0; i < G.vexnum; ++i)
if (indegree[i] == 0)
Push(S, i);
int index = 0; // 记录 topo 数组的下标
while (!StackEmpty(S))
{
int v;
Pop(S, v); // 弹出一个入度为 0 的结点的下标
topo[index] = v; // 将其保存在 topo 数组中
index++; // 指向 topo 数组下一个位置
// 将与之相邻的结点入度减一
for (ArcNode *p = G.vertices[v].firstarc; p; p = p->nextarc)
{
int k = p->adjvex; // k 为 v 的邻接点的下标
--indegree[k];
if (indegree[k] == 0) // 若入度减为0,则入栈
{
Push(S, k);
}
}
}
// 释放内存
delete[] indegree;
DestroyStack(S);
if (index < G.vexnum)
return false; //该有向图有回路
else
return true;
}
// G为邻接表存储的有向网,输出G的各项关键活动
bool CriticalPath(ALGraph &G)
{
int vexnum = G.vexnum; // 顶点个数
int arcnum = G.arcnum; // 边的条数
int *topo = new int[vexnum];
int *event_earliest = new int[arcnum];
int *event_latest = new int[arcnum];
if (!TopologicalOrder(G, topo))
return false;
// 初始化给每个事件的最早发生时间
// 源点的最早发生时间是 0, 其它结点就先来一个比较小的数占位, 就用 0 吧方便, 后面再更新
for (int i = 0; i < vexnum; i++)
event_earliest[i] = 0;
// 按拓扑次序求每个事件的最早发生时间
// 事件的最早发生时间起码要等到它的所有前驱事件结束
// A--(2)-->C B--(5)-->C , A最早4时刻发生, B最早2时刻发生, C最早发生得等AB中最迟完成的那个
// A结束:4+2=6 B结束:5+2=7 所以C最找得7时刻发生
for (int i = 0; i < vexnum; i++)
{
int k = topo[i]; // 取得拓扑序列中的顶点序号k
for (ArcNode *pre = G.inverse_vertices[k].firstarc; pre; pre = pre->nextarc) // 遍历 k 的所有前驱结点(事件)
{
int j = pre->adjvex; // j为前驱结点的序号
event_earliest[k] = max(event_earliest[k], event_earliest[j] + pre->weight);
}
}
// 初始化每个事件的最迟发生时间
// 汇点的最迟发生时间就是它的最早发生时间, 其它结点先来一个比较大的值, 后面再更新, 也用这个值吧
for (int i = 0; i < vexnum; i++)
event_latest[i] = event_earliest[vexnum - 1];
// 按逆拓扑次序求每个事件的最迟发生时间
// 事件的最迟发生时间就是 deadline, 再拖可能导致后面的事情做不完, 比如交作业前的最后一晚是写作业的最迟发生时间
// 要算一个事件的最迟发生时间, 得保证后续事件可以完成
// A--(2)-->B A--(1)-->C 假如 B 事件最迟6时刻必须开始, C在4时刻必须开始
// 为了保证B能完成,A拖到6-2=4时刻必须开始,为了保证C完成,A拖到4-1=3时刻必须开始, 所以A事件最迟发生时间是其中最早的一个,也就是3
for (int i = vexnum - 1; i >= 0; i--)
{
int k = topo[i]; // 取得逆拓扑序列中的顶点序号 k
for (ArcNode *p = G.vertices[k].firstarc; p; p = p->nextarc) // p指向 k 的后继结点(事件)
{
int j = p->adjvex; // j 为后继结点的序号
event_latest[k] = min(event_latest[k], event_latest[j] - p->weight);
}
}
// 判断每一活动是否为关键活动
cout << "关键活动路径为: ";
for (int i = 0; i < vexnum; i++)
{
for (ArcNode *p = G.vertices[i].firstarc; p; p = p->nextarc) // 处理 i 的所以后继结点(事件)
{
int j = p->adjvex; // j为后继事件的序号
int e = event_earliest[i]; // 活动<vi, vj>的最早开始时间就是事件 i 的最早开始时间
int l = event_latest[j] - p->weight; // 活动<vi, vj>的最迟开始时间是事件 j 的最迟开始时间 - 活动所需时间
if (e == l) // 若为活动的时间余量为 0, 说明<vi, vj>是关键活动
cout << G.vertices[i].data << "-->" << G.vertices[j].data << " ";
}
}
// 释放内存
delete [] topo;
delete [] event_latest;
delete [] event_earliest;
return true;
}
int main()
{
cout << "************算法6.13 关键路径算法**************" << endl;
ALGraph G;
CreateUDG(G);
cout << "有向图创建完成!" << endl;
if (!CriticalPath(G))
cout << "网中存在环,无法进行拓扑排序!" << endl;
cout << endl;
}