目录
5.利用ve[]和vl[]得到ee[]和el[],并判断得到关键路径
一、题目:关键路径寻找
- 对于给定的如下有向工程施工图,找出该工程图的关键路径。
- 输入:(始端结点 末端结点 权值)
1 4 5
1 3 4
1 2 6
2 5 1
3 5 1
4 6 2
5 8 7
5 7 9
6 8 4
7 9 2
8 9 4
- 输出:
关键路径:v1 v2 v5 v7 v8 v9
二、思路
1.图的存储
- 分为顶点表和边表部分,二者用一维数组adjlist[]连接起来。
- 顶点表里每一个顶点的信息包括:顶点vertex 入度id 指向第一次邻接结点的指针firstEdge;
- 边表每一个结点的信息包括:顶点adjvex 权值dut 指向下一个顶点的指针next
- 顶点表里每一个顶点的信息包括:顶点vertex 入度id 指向第一次邻接结点的指针firstEdge;
-
2.类的准备
- ·ve[] 顶点最早发生时间
- ·vl[] 顶点最晚发生时间
- ·ee[] 活动最早发生时间
- ·el[] 活动最晚发生时间
-
3.拓扑排序实现
初始化:将ve[i]通通置0
- 遍历所有顶点,将入度(id)=0的顶点入栈1(topo_in),即顶点v1作为源点入栈
- 弹出栈1顶元素,让此元素进入栈2,count+1遍历此顶点的邻接顶点,然后使得遍历的顶点入度(id)-1
- 以遍历到v2为例,此时i=1,j=2,j入栈1后,将ve[i]+p->dut与ve[j]相比较,如果要大,那就更新ve[j],p1指针继续后移
重复以上(2)(3),直到栈1为空。
- ·其中,最后count<vertexNnm(顶点数),则代表有环。
-
4.利用栈2逆拓扑顺序得到vl[],顶点的最晚发生时间
- 初始化:弹出拓扑排序最后一个顶点,汇点b。将vl[i]=ve[b]
- 栈2出栈得到始端结点i,p2指针指向i的邻接结点,顶点值用j来储存
- 判断相应vl[j]- p->dut > v[i], 或者vl[i] = 初始值ve[v],由于i的最晚发生时间=它的终端结点j的最晚发生时间-j的权值,p2指针后移,直到为空。
重复(1)(2),直到栈2为空
-
5.利用ve[]和vl[]得到ee[]和el[],并判断得到关键路径
- 准备:队列Q、out[]存储0、1,0表示未输出过。
- 令i=a(源点),先输出
- 栈中有元素出队,没有元素不出队,p3指向i的邻接结点,用j来存储p3所指的顶点指。队列Q中无重复结点,那就入队。
- 由规律可知:活动最早开始时间=顶点最早发生时间 即ee[k]=ve[i] (k是活动编号,由1->edgeNum) 而活动最晚发生时间el[k]=vl[j]-p3->dut
- 判断ee[k]==el[k],如果相等,即关键活动,且out[j]==0,即没有输出过,则输出它连接的终端结点vj。然后p3指针继续后移,k++
重复(2)(3)(4),直到k=edgeNum
三、代码实现
#include <iostream>
#include<stack>
using namespace std;
#include <fstream>
const int MaxSize = 15;
int visited[MaxSize] = { 0 };
//边表结构(邻接表结点):顶点v和它邻接的指针
struct EdgeNode
{
int adjvex; //顶点
int dut; //权值
EdgeNode* next; //指向下一个顶点的指针
};
template<typename DateType>
//顶点表的结构:存储顶点信息+头指针
struct VertexNode
{
DateType vertex; //顶点
int id; //入度
EdgeNode* firstEdge; //头指针
};
//构造ALGraph(用邻接表存储的图)这个类
template<typename DateType>
class ALGraph
{
public:
ALGraph(DateType a[], int n, int e, ofstream& ofs1); //建立顶点表一维数组
~ALGraph();
void BFTraverse(int v); //广度优先遍历图 v=1
void Topo_sort(); //拓扑排序并得到ve[]
void LTV(); //利用拓扑逆序,得到vl[]
void ETE_and_LTE(ofstream& ofs1); //计算活动的最早与最晚发生时间 v=a
private:
VertexNode<DateType> adjlist[MaxSize]; //最大顶点数
int vertexNum, edgeNum; //顶点个数,边数
int a; //源点,拓扑排序第一个点
int b; //汇点,拓扑排序最后一个点
int ve[MaxSize]={0}; //顶点最早发生时间
int vl[MaxSize]={0}; //顶点最晚发生时间
int ee[MaxSize]={0}; //活动最早发生时间
int el[MaxSize]={0}; //活动最晚发生时间
stack<DateType> topo_in; //栈1 ,用来入栈
stack<DateType> topo_out; //栈2,得到一个逆序拓扑排序
};
//构造函数,建立一维数组
template<typename DateType>
ALGraph<DateType>::ALGraph(DateType a[], int n, int e, ofstream& ofs1)
{
int i = 0, j = 0, k = 0, l = 0;
vertexNum = n; edgeNum = e;
int b[MaxSize] = { 0 };
EdgeNode* s = nullptr; //初始化
vertexNum = n; edgeNum = e; //将顶点数和边数传入
for (i = 1; i < vertexNum+1; i++) //初始化顶点表
{
adjlist[i].vertex = a[i];
adjlist[i].firstEdge = nullptr;
}
cout << "请依次输入每一条边:以1为第一个结点(始端结点 终端结点 权值):" << endl;
ofs1 << "始端结点 终端结点 权值:\t" << endl;
for (k = 1; k < edgeNum+1; k++) //依次输入每一条边
{
cin >> i >> j >> l; //输入便所依附的两个顶点的编号
ofs1 << "\n";
ofs1 << i << " " << j << " " << l << " ";
b[j]++; //终端结点对应的入度+1
s = new EdgeNode; //创建一个边表结点s
s->adjvex = j; //填入结点
s->dut = l;
s->next = adjlist[i].firstEdge; //将结点s插入到第i个边表的表头(头插法)
adjlist[i].firstEdge = s;
}
for (i = 1; i < vertexNum+1; i++)
{
adjlist[i].id = b[i];
}
}
//析构函数
template<typename DateType>
ALGraph<DateType>::~ALGraph()
{
EdgeNode* p = nullptr, * q = nullptr;
for (int i = 0; i < vertexNum; i++)
{
p = q = adjlist[i].firstEdge;
while (p != nullptr)
{
p = p->next;
delete q;
q = p;
}
}
}
template<typename DateType>
void ALGraph<DateType>::BFTraverse(int v)
{
int w, j, Q[MaxSize];
int front = -1, rear = -1; //初始化队列
EdgeNode* p = nullptr;
visited[v] = 1;
Q[++rear] = (v); //入列
topo_in.push(0);
while (front != rear) //队列不为空
{
w = Q[++front]; //出列
if (adjlist[w].id == 0)
{
topo_in.push(w);
a = w; //存储源点
}
p = adjlist[w].firstEdge;
while (p != nullptr)
{
j = p->adjvex;
if (visited[j] == 0)
{
visited[j] = 1;
Q[++rear] = j; //入队
}
p = p->next;
}
}
}
//实现思路
//重复以上过程直到用来入栈的栈空掉,且用来出栈的栈元素数目=vertexNum(计数器)
template<typename DateType>
void ALGraph<DateType>::Topo_sort()
{
int i;
int j;
int count = 0; //用于统计入栈2的顶点数
EdgeNode* p1 = nullptr;
//初始化:将ve[i]通通置0
for (i = 1; i < vertexNum; i++)
{
ve[i] = 0;
}
//1.遍历所有结点,将入度为0的顶点入栈,即v1
BFTraverse(1);
topo_out.push(0);
//2.弹出栈顶顶点,遍历此顶点的邻接顶点,然后使得遍历的邻接顶点入度-1,
while (topo_in.top()!=0) //重复以上过程直到用来入栈的栈空掉
{
i = topo_in.top();
topo_in.pop(); //出栈1
topo_out.push(i); //入栈2
count++;
p1 = adjlist[i].firstEdge;
while (p1 != nullptr)
{
j = p1->adjvex; //i-j
adjlist[j].id--; //入度-1
if (adjlist[j].id == 0)
{
topo_in.push(j); //入栈1
//3.以遍历到v2即1(i)-2(j)为例,入栈后,判断ve[i]+p(p指向2所在结点).dut
// 如果上值大于ve[2],更新ve[2]的值
// 否则,不更新
if ((ve[i] + p1->dut) > ve[j])
{
ve[j] = ve[i] + p1->dut;
}
}
p1 = p1->next;
}
}
//当拓扑排序结束后,判断此图是否存在环
//由于最后剩下环的时候,没有办法找到下一个入度为0的顶点,因此count会比verexNum小
if (count < vertexNum)
{
cout << "此图存在环,不能寻找关键路径" << endl;
exit(0);
}
}
//通过拓扑排序,我们得到了每一个顶点的最早发生时间ve[],接下来计算vl[]
//计算vl的值,即每一个顶点的最晚发生时间值,初始化vl[]为ve[]重最大的值,也就是汇点v9。
template<typename DateType>
void ALGraph<DateType>::LTV()
{
//1.初始化vl[]为ve[]重最大的值,也就是汇点v9。
int i = 1;
int j = 1;
b = topo_out.top(); //存储汇点,
EdgeNode* p2 = nullptr;
for (i; i < vertexNum + 1; i++)
{
vl[i] = ve[b];
}
topo_out.pop(); //令拓扑排序最后一个顶点,也就是汇点出栈
while (topo_out.top() != 0)
{
i = topo_out.top(); //2.就是访问之前的出栈序列(栈2)
topo_out.pop(); //出栈i,i作为始端结点
p2 = adjlist[i].firstEdge; //p2指向i的终端结点
while (p2 != nullptr)
{
j = p2->adjvex; //j即为终端结点
//判断相应vl[j]- p->dut > v[i], 或者vl[i] = 初始值ve[v]
if ((ve[j] - p2->dut) < vl[i] || vl[i] == ve[b])
{
vl[i] = vl[j] - p2->dut; //i的最晚发生时间=它的终端结点j的最晚发生时间-j的权值
}
p2 = p2->next; //继续下一个结点,直到为空
}
}
}
template<typename DateType>
void ALGraph<DateType>::ETE_and_LTE(ofstream& ofs1)
{
LTV();
int i = a, j = 0;
int k = 1;
int Q[MaxSize];
int front = -1, rear = -1; //初始化队列
EdgeNode* p3 = nullptr;
int out[MaxSize] = { 0 }; //用于检测该节点是否输出过,0表示未输出过
int c;
//v1要先输出
cout << "v" << i << " ";
ofs1 << "v" << i << " ";
while(k<edgeNum+1) //k=1->edgeNum
{
if (rear != -1)
{
i = Q[++front];
}
p3 = adjlist[i].firstEdge;
while (p3 != nullptr)
{
j = p3->adjvex;
if (rear == -1)
{
c = 0;
}
else
{
c = Q[rear];
}
//遍历邻接顶点j,用队列存储j
if (p3->adjvex != c)
{
Q[++rear] = j; //没有重复顶点,那就入队
}
ee[k] = ve[i]; //活动最早开始时间=顶点最早发生的时间
el[k] = vl[j] - p3->dut; //顶点最晚发生的时间-j的权值
//判断ee[i]?=el[i],若相等,输出vj
if (ee[k] == el[k])
{
if (out[j] == 0)
{
cout << "v" << j << " ";
ofs1 << "v" << j << " ";
out[j] = 1;
}
}
p3 = p3->next;
k++;
}
}
}
int main()
{
ofstream ofs1;
ofs1.open("ALGraph2.txt", ios::app);
int v = 0, e = 0, i = 0, j = 0;
cout << "请输入顶点数和边数:" << endl;
cin >> v >> e;
ofs1 << "顶点数:" << v << "\t" << "边数:" << e << endl;
cout << "请输入顶点:" << endl;
ofs1 << "具体顶点:" << endl;
int in[MaxSize];
for (i = 1; i < v+1; i++)
{
cin >> j;
in[i] = j;
ofs1 << j << " ";
}
//MG(顶点数组,顶点数,边数)
ALGraph<int>ALGraph(in, v, e,ofs1);
cout << "关键路径是:" << endl;
ofs1 << "\n关键路径是:" << endl;
ALGraph.Topo_sort();
ALGraph.ETE_and_LTE(ofs1);
ofs1.close();
system("pause");
return 0;
}
四、输出结果
五、体会
1.关键路径的查找过程中主要涉及四个概念,事件(顶点)的最早发生时间ETV,用ve[]存储数据,事件(顶点)的最晚发生时间LTV,用vl[]存储
2.活动(弧)的最早发生时间ETE,用ee[]存储;活动(弧)的最晚发生时间为LTE,用el[]存储。其中事件的最早发生时间通过拓扑排序获得,而事件的最晚发生时间是通过逆向访问顶点(从源点到汇点的方向)结合最早发生时间获得,弧的活动的最早与最晚发生时间则是通过遍历一遍所有的弧结合事件的最早与最晚发生时间获得。