一.什么是AOE-网络和关键路径?
AOE-网(Activity On Edge)即边表示活动的网。AOE-网是一个带权的有向无环图,其中顶点表示事件(Event),弧表示活动,权代表活动持续的时间。通常AOE-网可以用来估算工程的完成时间。
正常情况下,网中只有一个入度为0的点(称作源点)和一个出度为0的点(叫做汇点)。
由于AOE-网中有些活动可以并行的进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度(这里说的路径长度是指路径上各活动持续时间之和,不是路径上弧的数目)。路径长度最长的叫做关键路径(Critical Path)。
二.怎么求关键路径?
首先我们需要明确,由关键活动组成的路径就是关键路径
1.那么什么又是关键活动呢? 该活动的早发生时间和最晚发生时间相同
2.那如何求一个活动的最晚开始时间和最早开始时间呢?
活动的最早开始时间 = 该活动的始点对应的事件发生的最早时间
活动的最晚开始时间 = 该活动的终点对应的事件发生的最晚时间 - 该活动花费的时间
3.那如何求事件发生的最早开始时间和最晚开始时间呢?
这里我们需要知道源点默认事件的最早开始时间和最晚开始时间为0,汇点的最早开始时间和最晚开始时间为关键路径的长度。在一个AOE-网中,一个活动连接两个事件。
事件的最早发生时间 = MAX{以该事件作为终点的边的始点的事件的最早发生时间 + 边的权值}
事件的最晚发生时间 = MIN{以该事件作为始点的边的终点的事件的最晚发生时间 - 边的权值}
4.这里我们以ve[i]表示某事件的最早发生时间,ve[j]表示以i为始点的边对应的终点,vl[i]表示某事件的最晚发生时间,ee[i]表示某活动的最早发生时间,el[i]表示某活动的最晚发生时间,dut<i,j>表示以i为始点,j为终点的边的权值。故以上公式可以简写为:
ve[j] = MAX{ve[i] + dut<i,j>}
vl[i] = MIN{vl[j] - dut<i,j>}
ee[i] = ve[i]
el[i] = vl[j] - dut<i,j>
5.算法的具体实现思想
(1)从源点出发,按拓扑排序求其余各顶点的最早发生时间。其次拓扑排序还可以检查该该图是否存在环,若该有向图存在环则无法求解关键路径。
(2)从汇点出发,按逆拓扑有序求其余各顶点的最迟发生时间。
(3)根据各顶点的ve和vl值求各个活动发生的最早发生时间和最晚发生时间,若该活动的最晚发生时间和最早发生时间相同,则该活动为关键活动。
三.java代码实现该算法
import java.util.Scanner;
import java.util.Stack;
/*
领接表存储AOE-网
9 11
1 2 1 6
1 3 2 4
1 4 3 5
2 5 4 1
3 5 5 1
4 6 6 2
5 7 7 9
5 8 8 7
6 8 9 4
7 9 10 2
8 9 11 4
*/
public class AdjacencyList {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int numberOfNode = sc.nextInt(); //点的总数
int numberOfEdge = sc.nextInt(); //边的总数
VNode[] v = new VNode[numberOfNode]; //创建领接表的数组部分,事件的名字的起始点从1开始
int[] indegree = new int[numberOfNode]; //创建一个入度数组,数组同样是从下标为0开始计数
for(int i=0;i<numberOfNode;i++) {
v[i] = new VNode(i+1,null); //初始化事件的标号,从下标为1开始
}
//例样输入:1 2 1 6 表示起始点为1,终止点为2,活动名为1,活动持续时间为6天
for(int i=1;i<=numberOfEdge;i++) { //录入各条边
int start = sc.nextInt();
int end = sc.nextInt();
int name = sc.nextInt();
int weight = sc.nextInt();
ArcNode tempArc = new ArcNode(weight,name,end-1,null); //初始化边结点
if(v[start-1].fistEdge != null) {
tempArc.nextArc = v[start-1].fistEdge;
v[start-1].fistEdge = tempArc;
} else {
v[start-1].fistEdge = tempArc;
}
indegree[end-1]++;
}
sc.close();
ToPoLogicSort(indegree,v,numberOfEdge);
}
/*拓扑排序判断该有向图是否存在环,传入参数为入度数组和图的领接表,同时完成我们ve[]数组
*为了完成整个关键路径的编写,我们必须引入4个数据结构
*Ve[] 表示各个事件最早开始的时间
*Vl[] 表示各个事件最晚开始的时间
*Ee[] 表示各个活动最早开始的时间
*El[] 表示各个活动最晚开始的时间
*/
public static boolean ToPoLogicSort(int[] indegree,VNode[] v,int numberOfEdge) {
Stack<VNode> s = new Stack<>(); //为了避免重复检测入度为零的顶点,我们另设一栈来存储入度为0的顶点
Stack<Integer> si = new Stack<>(); //此栈的创建完全是为了求Vl数组的值
int[] ve = new int[indegree.length]; //存储各个事件最早可以开始的时间,并完成初始化
for(int i=0;i<ve.length;i++) {
ve[i] = 0;
}
for(int i=0;i<indegree.length;i++) { //将入度为零的点入栈
if(indegree[i] == 0) {
s.push(v[i]);
}
}
int count = 0;
while(!s.isEmpty()) {
VNode tempN = s.pop();
System.out.print(tempN.name+" ");
si.push(tempN.name); //将拓扑排序序列保存到栈中,注意这里保存的是事件的名字
count++;
ArcNode p = tempN.fistEdge;
while(p != null) {
//完成入度数组的更新
int index = p.nextVode;
indegree[index]--;
if(indegree[index]==0) {
s.push(v[index]);
}
//完成ve数组的填写
if(ve[tempN.name-1]+p.weight > ve[p.nextVode]) {
ve[p.nextVode] = ve[tempN.name-1]+p.weight;
}
p = p.nextArc;
}
}
//如果count的值小于顶点的个数,说明该图存在环,不可以求关键路径
if(count<indegree.length) {
return false;
}
//开始求vl数组的值
int[] vl = new int[indegree.length];
for(int i=0;i<vl.length;i++) {
vl[i] = ve[ve.length-1]; //初始化vl数组,将其初始化为汇点的值,在AOE-网中汇点的值明显最大
}
si.pop(); //首先弹出汇点,因为汇点的最早和最晚时间相同
while(!si.isEmpty()) {
int tempN = si.pop()-1; //由于栈中存储的是事件的名字,将其转化为数组的下标
ArcNode tempArc = v[tempN].fistEdge;
while(tempArc != null) {
if(vl[tempArc.nextVode]-tempArc.weight < vl[tempN]) {
vl[tempN] = vl[tempArc.nextVode]-tempArc.weight;
}
tempArc = tempArc.nextArc;
}
}
System.out.println();
//下面开始求活动的最早开始时间和活动的最晚开始时间
int[] ee = new int[numberOfEdge]; //活动有11个,为11条边
int[] el = new int[numberOfEdge];
System.out.println("该图的关键路径为:");
for(int i=0;i<v.length;i++) {
ArcNode tempArc = new ArcNode();
tempArc = v[i].fistEdge;
while(tempArc != null) {
ee[tempArc.name-1] = ve[i]; //活动的最早开始时间等于时间开始的最早时间
el[tempArc.name-1] = vl[tempArc.nextVode]-tempArc.weight; //活动的最晚开始时间,等于其终点的最晚开始时间减去边的权值
//输出关键活动
if(ee[tempArc.name-1] == el[tempArc.name-1]) {
System.out.print(tempArc.name+"<"+v[i].name+","+(tempArc.nextVode+1)+"> ");
}
tempArc = tempArc.nextArc;
}
}
return true;
}
//展示领接表函数,观察图是否存储正确
public static void display(VNode[] v) {
for(int i=0;i<v.length;i++) {
System.out.print("事件"+(i+1)+" ");
ArcNode Arc = v[i].fistEdge;
while(Arc != null) {
System.out.print("活动"+Arc.name+" ");
Arc = Arc.nextArc;
}
System.out.println();
}
}
}
//顶点的结构体
class VNode{
int name; //事件的名字
ArcNode fistEdge; //第一条边
public VNode() {
super();
}
public VNode(int name, ArcNode fistEdge) {
super();
this.name = name;
this.fistEdge = fistEdge;
}
}
//弧的结构体
class ArcNode{
int weight;
int name; //该活动的编号
int nextVode; //该活动的另一个端点
ArcNode nextArc;
public ArcNode() {
super();
}
public ArcNode(int weight,int name,int nextVode,ArcNode nextArc) {
this.weight = weight;
this.name = name;
this.nextVode = nextVode;
this.nextArc = nextArc;
}
}