1.实验内容
用字符文件提供数据建立AOE网络合适的存储结构。计算工程的最短完成时间并输出所有的关键路径。输出的每条关键路径用顶点序号或字母的拓扑有序序列表示。输出结果用另一字符文件保存。
2.实验背景
1. 什么是AOE网络?
AOE(Activity on Edge)网络是带权的有向无环图 (DAG),其中,顶点表示事件(event),弧表示活动 (activity),弧上的权值表示活动持续的时间。通常, AOE网用来估算工程的完成时间。 一般说来,整个工程只有一个开始点和一个结束 点,即正常情况下,网中只有一个入度为0的顶点(称 为源点)和一个出度为0的顶点(称为汇点)。
针对AOE网研究的问题是:
a. 完成整个工程至少需要多少时间(最短时间);
b. 哪些活动是影响工程进度的关键。
2. 什么是关键路径?
AOE网中,从源点到汇点的所有路径中,边上权值之和最大的路径称为关键路径。显然,关键路径上的权值之和就是完成工程所需的最短时间。
注意:关键路径可能不止一条。
3.程序运行示例与结果
AOE网络与关键路径示例:
用字符文件提供该AOE网络的相关信息
9 11
0 1 6
0 2 4
0 3 5
1 4 1
2 4 1
3 5 2
4 6 9
4 7 7
5 7 4
6 8 2
7 8 4
其中第一行的两个数分别代表AOE网络中事件数n和弧数e
而后的e行,每一行表示每一段弧的起始与终点的信息与弧的权重
最终输出结果如下:
工程最快的完成时间为:18
路径为:0->1->4->6->8
路径为:0->1->4->7->8
4.实验设计
1.根据字符文件创建图的邻接表存储结构
用链表实现图的存储结构:以顶点vi为弧尾的所有弧组成的集合用单链表实现存储,头结点存储弧尾vi 的编号和信息,其余结点存储弧头顶点编号、弧的权重和信息。
建立后的结果:
0->1->2->3
1->4
2->4
3->5
4->6->7
5->7
6->8
7->8
头结点(存储顶点vi ):
//存储顶点
typedef struct
{ //顶点数据
int data;
int i; //顶点下标
ArcNode* firstarc;
} HNode;
表结点(存储边或弧):
//存储边或弧
typedef struct node
{
double w;
int j; //邻接点下标
struct node* nextarc;
} ArcNode;
图的数据结构:
//存储图邻接表
typedef struct
{
HNode h[100]; //头结点形成数组
int n, e; //n:实际顶点数; e:边或弧的数目
} ALGraph;
首先读入顶点数与弧数,然后依次读入弧的信息创建存储结构
//创建图的邻接结构
void crtGraph2(ALGraph& G)
{
int i, j, w, k;
ifstream file;
file.open("E:\\大学学习\\数据结构\\AOE.txt", ios::in);
file >> G.n >> G.e; //读入顶点数和弧数
for (i = 0; i < G.n; i++)
{
G.h[i].i = i; //初始化顶点序号
G.h[i].firstarc = NULL;
}
for (k = 0; k < G.e; k++)
{
ArcNode* pr, * p, * q = new ArcNode;
file >> i >> j >> w;
q->j = j; q->nextarc = NULL; //建第i行表结点并插入
pr = NULL; p = G.h[i].firstarc;
while (p && p->j < j) //推进指针使结点从小到大找到该插入的位置
{
pr = p;
p = p->nextarc;
}
if (pr == NULL)//如果这为第一个结点
{
q->nextarc = p;
G.h[i].firstarc = q;
q->w = w;
}
else //在中间插入
{
pr->nextarc = q;
q->nextarc = p;
q->w = w;
}
}
}
2.求解关键路径
(1) 定义如下辅助数据
ve(i): 顶点vi 的最早开始时间
vl (i): 顶点vi 的最晚开始时间
w: 活动的持续时间,即该弧上的权值
e(i, j): 活动的最早开始时间
l(i, j): 活动的最晚开始时间
为保证工程完成时间尽可能短,令ve(汇点)=vl (汇点) 显然,我们有 e(i, j)=ve(i), l(i, j)=vl (j)-w 即,活动的最早和最晚开始时间可由它所依附的顶点的最早开始和最晚开始时间计算。
对任一活动,若 e(i, j)=l(i, j),说明活动不能延迟,否则整个工 程会延迟,于是称为关键活动; e(i, j)延迟0~l(i, j)-e(i, j)时间, 不会影响整个工程进度。
性质:关键路径上的活动都是关键活动。
(2) 怎样求关键路径?
求关键路径须求得所有活动(弧)的e(i, j)和l(i, j), 以确定其中哪些是关键活动。而求e(i, j)和l(i, j)需先求得所有顶点(事件)的ve(i)和vl(i)。
(3) 怎样求ve(i)和vl (i)?
为表述方便,设AOE网络共有n个顶点,且源点为vs,汇点为vt 。
a. 第1步,前向递推计算所有顶点的最早开始时间 ve(s)=0,ve(j)=max{ve(i)+w(i,j)} (前面可能会有多个事件,取最大值)其中,i满足vi 属于{邻接至vj 的所有顶点组成的集合};下标 j 的次序为全部顶点的一个拓扑有序序列。 j按拓扑有序递推计算上式,可确保在计算每个 ve(j)之前,邻接至vj 的所有顶点vi 的最早开始时间ve(i) 已经得到。故,前向递推可由拓扑排序算法实现。
b. 第2步,反向递推计算所有顶点的最晚开始时间 vl (t)=ve(t) ,vl(i)=min(vl(j)-w(i.j))(后面可能会有多个事件,取最小值)其中,j满足vj 属于{邻接自vi 的顶点集合}; 下标 i 的次序为全部顶点的一个逆拓扑有序序列。 i按逆拓扑有序递推计算上式,可确保在计算每个vl (i)之前,邻接自vi 的所有顶点vj 的最晚开始时间 vl (j)已经得到。
前向递推时,顶点按拓扑有序次序入栈,则反向递推时,顶点依次退栈即得到逆拓扑有序序列。
int CalVex(ALGraph& G, double ve[], double vl[])
//返回值为源点下标(-1表示有环); ve[], vl[]为结果
{ //首先创建并初始化indegree数组
int* indegree = new int[G.n];
int i, j, k, count; ArcNode* p; Stack S, T;
for (i = 0; i < G.n; i++) indegree[i] = 0;
for (i = 0; i < G.n; i++)
for (p = G.h[i].firstarc; p != NULL; p = p->nextarc)
indegree[p->j]++;
//以下开始前向递推(拓扑排序)
InitStack(S); InitStack(T);
//所有顶点的最早开始时间初始化为0
for (i = 0; i < G.n; i++) { ve[i] = 0; }
for (i = 0; i < G.n; i++) //源点入栈(这里假定源点仅1个)
if (!indegree[i])
{ Push(S, i); k = i; break; }
count = 0; //k保存源点下标
while (!EmptyStack(S))
{
i = Pop(S); Push(T, i); count++;
for (p = G.h[i].firstarc; p; p = p->nextarc)
{
if (!(--indegree[p->j])) Push(S, p->j);
double w = ve[i] + p->w;
if (ve[p->j] < w) ve[p->j] = w;
} //此处实现前向递推公式的方法
}
delete[]indegree;
if (count < G.n) return -1;
//以下开始反向递推
if (!EmptyStack(T)) j = Pop(T); //汇点出栈
//所有顶点最晚开始时间初始化为汇点最早开始时间
for (i = 0; i < G.n; i++) vl[i] = ve[j];
while (!EmptyStack(T))
{
i = Pop(T);
for (p = G.h[i].firstarc; p; p = p->nextarc)
{
double w = vl[p->j] - p->w;
if (vl[i] > w) vl[i] = w;
}
}
return k; //返回源点下标
}
此时得到:
ve:0 6 4 5 7 7 16 14 18
vl:0 6 6 8 7 10 16 14 18
(4):记录关键路径上的点
//记录关键路径上的点
void RecKA(ALGraph& G, double ve[], double vl[], bool *visited)
{
int ee;
int ll;
for (int i = 0; i < G.e; i++)
{
visited[i] = 1;//先假设所有点都不需要被访问
}
for (int i = 0; i < G.n; i++)
{
for (ArcNode* p = G.h[i].firstarc; p; p = p->nextarc)
{
ee = ve[i];
ll = vl[p->j] - p->w;
if (ee == ll)//如果为关键活动
{
visited[i] = 0;
visited[p->j] = 0;//两个点都需要被访问
}
}
}
}
得到的visited数组:0 0 1 1 0 1 0 0 0
3.输出所有关键路径
定义一个堆栈以及相关关于栈的操作
typedef struct {
int elem[100];
int top;
int c;
int n;
}Stack;//定义栈
void InitStack(Stack& a)//初始化栈
{
a.top = -1;
a.c = 100;
a.n = 0;
}
void Push(Stack& a, int e)//压栈
{
a.top++;
a.elem[a.top] = e;
}
int Pop(Stack& a)//弹出
{
int e;
e = a.elem[a.top];
a.top--;
return e;
}
int EmptyStack(Stack& a)//判断栈是否为空
{
return a.top == -1;
}
从开始结点开始使用DFS深度优先遍历(递归)扫描整个AOE网络,如果与起点相连的结点需要被访问,那么将结点压入栈,继续深度优先遍历访问,当访问到底的时候,将栈中的所有元素打印在文件中,即为其中一条关键路径,退栈,直到深度优先遍历完成结束。
void DFS(ALGraph& G, bool visited[], int i, Stack &u, ofstream &file2)
//从vi出发深度优先搜索
{
Push(u, i);
visited[i] = true;
visited[G.n - 1] = 0;
if (i == G.n - 1)//遍历到底了
{
file2 << "路径为:";
for (int k = 0; k < u.top; k++)//打印
{
file2 << u.elem[k] << "->";
}
file2 << u.elem[u.top] << endl;
}
for (ArcNode* j = G.h[i].firstarc; j != NULL; j = j->nextarc)
{
if (!visited[j->j])
{
DFS(G, visited, j->j, u, file2);
Pop(u);
}
}
}
最终输出结果为:
工程最快的完成时间为:18
路径为:0->1->4->6->8
路径为:0->1->4->7->8
5.完整源程序如下:
#include<iostream>
#include<fstream>
using namespace std;
//存储边或弧
typedef struct node
{
double w;
int j; //邻接点下标
struct node* nextarc;
} ArcNode;
//存储顶点
typedef struct
{ //顶点数据
int data;
int i; //顶点下标
ArcNode* firstarc;
} HNode;
//存储图邻接表
typedef struct
{
HNode h[100]; //头结点形成数组
int n, e; //n:实际顶点数; e:边或弧的数目
} ALGraph;
//创建图的邻接结构
void crtGraph2(ALGraph& G)
{
int i, j, w, k;
ifstream file;
file.open("E:\\大学学习\\数据结构\\AOE.txt", ios::in);
file >> G.n >> G.e; //读入顶点数和弧数
for (i = 0; i < G.n; i++)
{
G.h[i].i = i; //初始化顶点序号
G.h[i].firstarc = NULL;
}
for (k = 0; k < G.e; k++)
{
ArcNode* pr, * p, * q = new ArcNode;
file >> i >> j >> w;
q->j = j; q->nextarc = NULL; //建第i行表结点并插入
pr = NULL; p = G.h[i].firstarc;
while (p && p->j < j) //推进指针使结点从小到大找到该插入的位置
{
pr = p;
p = p->nextarc;
}
if (pr == NULL)//如果这为第一个结点
{
q->nextarc = p;
G.h[i].firstarc = q;
q->w = w;
}
else //在中间插入
{
pr->nextarc = q;
q->nextarc = p;
q->w = w;
}
}
}
typedef struct {
int elem[100];
int top;
int c;
int n;
}Stack;//定义栈
void InitStack(Stack& a)//初始化栈
{
a.top = -1;
a.c = 100;
a.n = 0;
}
void Push(Stack& a, int e)//压栈
{
a.top++;
a.elem[a.top] = e;
}
int Pop(Stack& a)//弹出
{
int e;
e = a.elem[a.top];
a.top--;
return e;
}
int EmptyStack(Stack& a)//判断栈是否为空
{
return a.top == -1;
}
int CalVex(ALGraph& G, double ve[], double vl[])
//返回值为源点下标(-1表示有环); ve[], vl[]为结果
{ //首先创建并初始化indegree数组
int* indegree = new int[G.n];
int i, j, k, count; ArcNode* p; Stack S, T;
for (i = 0; i < G.n; i++) indegree[i] = 0;
for (i = 0; i < G.n; i++)
for (p = G.h[i].firstarc; p != NULL; p = p->nextarc)
indegree[p->j]++;
//以下开始前向递推(拓扑排序)
InitStack(S); InitStack(T);
//所有顶点的最早开始时间初始化为0
for (i = 0; i < G.n; i++) { ve[i] = 0; }
for (i = 0; i < G.n; i++) //源点入栈(这里假定源点仅1个)
if (!indegree[i])
{ Push(S, i); k = i; break; }
count = 0; //k保存源点下标
while (!EmptyStack(S))
{
i = Pop(S); Push(T, i); count++;
for (p = G.h[i].firstarc; p; p = p->nextarc)
{
if (!(--indegree[p->j])) Push(S, p->j);
double w = ve[i] + p->w;
if (ve[p->j] < w) ve[p->j] = w;
} //此处实现前向递推公式的方法
}
delete[]indegree;
if (count < G.n) return -1;
//以下开始反向递推
if (!EmptyStack(T)) j = Pop(T); //汇点出栈
//所有顶点最晚开始时间初始化为汇点最早开始时间
for (i = 0; i < G.n; i++) vl[i] = ve[j];
while (!EmptyStack(T))
{
i = Pop(T);
for (p = G.h[i].firstarc; p; p = p->nextarc)
{
double w = vl[p->j] - p->w;
if (vl[i] > w) vl[i] = w;
}
}
return k; //返回源点下标
}
//记录关键路径上的点
void RecKA(ALGraph& G, double ve[], double vl[], bool *visited)
{
int ee;
int ll;
for (int i = 0; i < G.e; i++)
{
visited[i] = 1;//先假设所有点都不需要被访问
}
for (int i = 0; i < G.n; i++)
{
for (ArcNode* p = G.h[i].firstarc; p; p = p->nextarc)
{
ee = ve[i];
ll = vl[p->j] - p->w;
if (ee == ll)//如果为关键活动
{
visited[i] = 0;
visited[p->j] = 0;//两个点都需要被访问
}
}
}
}
void DFS(ALGraph& G, bool visited[], int i, Stack &u, ofstream &file2)
//从vi出发深度优先搜索
{
Push(u, i);
visited[i] = true;
visited[G.n - 1] = 0;
if (i == G.n - 1)//遍历到底了
{
file2 << "路径为:";
for (int k = 0; k < u.top; k++)//打印
{
file2 << u.elem[k] << "->";
}
file2 << u.elem[u.top] << endl;
}
for (ArcNode* j = G.h[i].firstarc; j != NULL; j = j->nextarc)
{
if (!visited[j->j])
{
DFS(G, visited, j->j, u, file2);
Pop(u);
}
}
}
int main()
{
ALGraph G;
crtGraph2(G);//建立图
double* ve = new double[G.n];//顶点vi的最早开始时间
double* vl = new double[G.n];//顶点vi的最晚开始时间
CalVex(G, ve, vl);
ofstream file2;
file2.open("E:\\大学学习\\数据结构\\res.txt", ios::out);
file2 << "工程最快的完成时间为:" << ve[G.n - 1] << endl;
bool* visited = new bool[G.n];//判断是否需要被访问的数组
Stack u;
InitStack(u);
RecKA(G, ve, vl, visited);//记录关键路径上的点
DFS(G, visited, 0, u, file2);//深度优先遍历
return 0;
}