目录
题目描述
学习流程
拓扑排序
关键路径理论
算法实现
数据结构和全局变量定义,main函数
建立邻接表
拓扑排序并计算ve
遍历拓扑逆序列计算vl,e,l,求关键路径
说明: AOE 网络是有向无环加权图,其中顶点表示事件,弧表示活动,权表示活动持续的时间,通常可以用来估算工程完成的时间,即图中从开始点到结束点之间最长的路径对应的时间。请完成一个程序,完成下列任务:
1 、计算 AOE 网络对应的拓扑排序。如果排序结果不唯一,请输出按照从小到大的顺序排列的结果。从小到大的顺序就是输入的节点序列顺序(参见下面关于输入格式的说明)。如图1中满足要求的拓扑排序是: a-b-c-d-e-f-g-h-k ,图2中满足要求的拓扑排序是:v1-v3-v5-v2-v6-v4-v7-v8-v9
2 、计算 AOE 网络的关键路径。注意关键路径可能不唯一,要求输出所有的关键路径。同样,按照是按照从小到大的顺序输出。例,如果得到两条关键路径,分别是0-1-3-6-8-9和0-1-3-4-5-8-9,那么先输出后一条路径,因为两条路径中前三个节点相同,而后一条路径第四个节点的编号小。
测试用例的输入输出格式说明:
输入:
9,11
a,b,c,d,e,f,g,h,k
<0,1,6>,<0,2,4>,<0,3,5>,<1,4,1>,<2,4,1>,<4,6,8>,<4,7,7>,<3,5,2>,<5,7,4>,<6,8,2>,<7,8,4>
节点的个数,边的条数;
各个节点的名称序列
边: < 起点 , 终点 , 权值 > 。说明起点和终点是在各个点在输入序列中的位置,如图1中边 表示为 <0,1,6> 。
输出:
关键路径
from 0(a) to 1(b) w 6
from 1(b) to 4(e) w 1
from 4(e) to 7(h) w 7
from 7(h) to 8(k) w 4
先把拓扑排序学了,这个流程不难,无非是把源头节点和对应的弧删掉,如果其中暴露出新的源头节点,就把新源头节点再进行这样的操作,循环直到结束。
关键是拓扑排序的理解。
拓扑排序排出来的到底是个什么顺序?
其实关键就是,凡是出栈的节点,其前置节点必然已经被访问过了!
所以拓扑排序的本质就是按照事情的前后顺序,把需要先完成的事情放在前面,只要顺着走下来,肯定不会碰到没有完成前置条件而无法进行下去的情况。
什么是关键路径?
直观理解就是最耗时的一条路径,不完成这一条,走了别的也没用。
从严蔚敏《数据结构》里看,有四个变量ve,vl,这两个是事件最早和最晚完成的时间,如果要完成一件事,必须要完成之前的所有事情。
显而易见,设target为汇点,则ve(target)就是完成所有任务的最早时间。
这个时候我们先别管任务,只管事件,所以我们继续。
从我们实际角度理解,最早完成不就是从头开始不断往后推嘛,所以我们也很好理解ve的递推算法,ve(j)=max{ve(i)+w(i,j)},实际是以j为头,在所有的路径里找一条最长的。
同理,大家计算最晚完成也轻车熟路了,ddl怎么算我们就怎么算,从任务截止(最后一件事完成之前)往前推,vl(i)=min{vl(j)-w(i,j)},实际是以i为尾,在所有路径里找一条最长的。
为什么都是最长的?因为我们所有的假设都是在所有任务都完成的前提之下的,所以要走消耗最大的路径就可以代表所有路径。
好,ve和vl说完了,该说e和l了
e是活动最早开始的时间,自然是从前往后推,i事件一旦完成,那就可以马上进行活动,所以e(i,j)=ve(i)
l是活动最晚开始的时间,自然是从后往前推,合情合理分析,一个活动最晚开始的时间+消耗时间是不是就是下一个事件的最晚完成时间?如果活动最晚开始时间往后推,那么下一个事件的最晚完成时间也要后推,整个活动都要后推。
把上式变形:l(i,j)=vl(j)-w(i,j);
为什么要先进行拓扑排序?
我们所有事件ve的计算都是建立在前置事件已经达成的基础上,而拓扑排序本质就是进行一个完成顺序的排序,所以完美契合。
//使用邻接链表,进行拓扑排序和关键路径计算
#include
#include
#include
#include
#include
#define MAX_V 20
typedef struct Arc {
int hver;
int weight;
struct Arc* nextarc;
}Arc;
typedef struct Ver {
char data;
struct Arc* firstarc;
}Ver;
typedef struct Gragh {
Ver vertexes[MAX_V];
int vernum, arcnum;
}Gragh;
int in_arcs[MAX_V] = { 0 };
int ve[MAX_V] = { 0 };
int vl[MAX_V] = { 0 };
int main(void)
{
Gragh G;
std::stack T;//储存topologicalOrder的逆序列
char path[MAX_V];//储存关键路径
freopen("input.txt", "r", stdin);
creatAdjList(G);
puts("新建表后的情况");
printAdjList(G);
topologicalOrder(G, T);
puts("拓扑排序并计算ve后情况");
printAdjList(G);
criticalPath(G, T, path);
puts("计算了vl以及e,l后");
printAdjList(G);
return 0;
}
没什么好说的,就是多加了个入度计算。
void creatAdjList(Gragh& G)//构造有向图,并且计算入度
{
scanf("%d,%d\n", &G.vernum, &G.arcnum);
for (int i = 0; i < G.vernum; i++)//读取节点
{
//不用初始化inarc
scanf("%c", &G.vertexes[i].data);
G.vertexes[i].firstarc = NULL;
getchar();//清理一下分隔符
}
for (int i = 0; i < G.arcnum; i++)//读取arc
{
int v1, v2, w;
scanf("<%d,%d,%d>", &v1, &v2, &w);
//增加弧
Arc* arc = (Arc*)malloc(sizeof(Arc));
if (!arc) exit(0);
arc->nextarc = G.vertexes[v1].firstarc;
G.vertexes[v1].firstarc = arc;
arc->hver = v2;
arc->weight = w;
//增加入度
in_arcs[v2]++;
getchar();//清理分隔符
}
}
void printAdjList(Gragh G)
{
for (int i = 0; i < G.vernum; i++)
{
printf("in %d\t ve %d\t le %d\t data %c ",
in_arcs[i], ve[i], vl[i], G.vertexes[i].data);
Arc* arc = G.vertexes[i].firstarc;
while (arc)
{
printf("%d(%c) weight %d\t", arc->hver,
G.vertexes[arc->hver].data, arc->weight);
arc = arc->nextarc;
}
putchar('\n');
}
}
这个和我们之前遇到的有所不同,因为要额外计算个ve,vl,e,l,入度。
解决办法之一就是加全局变量,前提是数据结构已经定死了不可变了(然而实际上完全可以在vertex里加ve,vl,in_arcs,在arc里加e,l),我们采取全局变量。(虽然我一直觉得这就像脱裤子放屁一样,程序的原子性已经被破坏)
算法:
将所有0入度点加入S栈,然后进行非空栈循环
{
首先弹出S的top,把S->T,T用来储存拓扑逆序列。同时counter++,计数判断是否有环
注意这个时候我们的ve已经知道了,因为凡是在栈顶的,其前置任务一定都完成了
那么我们用这个ve去计算后面点的ve也就完全逻辑无误了。
遍历弧。遍历弧的时候就要增加一步
{
给下一个事件的入度-1,如果为0,就入S
下一个点的计算下一个点的ve如果是更大的,就覆盖
(这样就保证能最后弄出最大的ve)
}
}
void topologicalOrder(Gragh& G, std::stack& T)//拓扑排序,求ve,并记录拓扑序列
{
int counter = 0;//计数,判断环
std::stack S;//临时用
//将源点位置加入S
for (int i = 0; i < G.vernum; i++)
{
if (in_arcs[i] == 0)
S.push(i);
}
//进行空栈循环
while (!S.empty())
{
int top = S.top();
T.push(top);
S.pop();
counter++;
Arc* arc = G.vertexes[top].firstarc;
while (arc)
{
int target = arc->hver;//头部
if (--in_arcs[target] == 0)//删除arc并且判断为0增加新源点
S.push(target);
int temp_ve = ve[top] + arc->weight;//计算下一个点ve
if (temp_ve > ve[target])
ve[target] = temp_ve;
arc = arc->nextarc;
}
}
if (counter != G.vernum)
puts("loop");
}
逆向求出vl
然后计算每条arc的e,l,如果相等就记录在path数组里,同时也可以选择打印出来。
//先计算vl
初始化。先给所有vll赋值,赋最后一个ve即可,因为最后一个点相当于最早完成和最晚完成时间是相同的。
用T逆序计算vl
//计算e,l,找到关键步骤
遍历所有arc,算e,l,如果e==l,就输出
void criticalPath(Gragh G, std::stackT, char path[])//求关键路径,先使用拓扑逆序列求le
{
//用逆序列求vl
for (int i = 0; i < G.vernum; i++)//初始化,令最大
{
vl[i] = ve[G.vernum - 1];
}
while (!T.empty())
{
int top = T.top();
T.pop();
Arc* arc = G.vertexes[top].firstarc;
while (arc)
{
int target = arc->hver;
int temp_vl = vl[target] - arc->weight;
if (temp_vl < vl[top])
vl[top] = temp_vl;
arc = arc->nextarc;
}
}
//计算所有arc的e,l,将e==l的记录在path中
int p = 0;//指向path尾部
for (int i = 0; i < G.vernum; i++)
{
Arc* arc = G.vertexes[i].firstarc;
while (arc)
{
int target = arc->hver;
int e = ve[i];
int l = vl[target] - arc->weight;
if (e == l)
{
printf("from %d(%c) to %d(%c) w %d\n", i, G.vertexes[i].data,
target, G.vertexes[target].data, arc->weight);
path[p++] = i;
}
arc = arc->nextarc;
}
}
}
但是我们这里有一些情况,就是,如果有多个关键任务,其实都是会被记录下来的,理论上可以有多条关键路径,比如下面这组数据
4,4
a,b,c,d
<0,1,5>,<0,2,5>,<1,3,2>,<2,3,2>
就会输出这个结果
from 0(a) to 2(c) w 5
from 0(a) to 1(b) w 5
from 1(b) to 3(d) w 2
from 2(c) to 3(d) w 2
那么也就是说我们需要利用求得的关键任务来组合形成关键路径,这就可能会产生多条。
具体判断方法可以是,用这些再构建一个图,然后进行dfs或者bfs,找出所有路径,我懒得实现了,放篇文章即可。
遍历寻找给定两点之间的所有路径_tan_chi_she的博客-CSDN博客_遍历两点之间所有路径