一、AOV网和AOE网
AOE网和AOV网都是有向无环图,
如果给定AOV网中各顶点活动所需要的时间,那么就可以将AOV网转换为AOE网
(比较简单的方法是:
- 将AOV网中的每个顶点都拆成两个顶点,分别表示活动的起点和终点,
- 两个顶点通过有向边连接,该有向边表示原顶点的活动,边权为需要的时间
- 原AOV网中的边全部视为空活动,边权为0(因为AOE结点表示已完成)
总花费时间=到达终点的多条路径中花费时间最长的那条
关键路径(要达到最短花费时间,不能拖延的路径):AOE网中的最长路径
二、最长路径
最长路径:
1.对一个没有从源点可达的正环的图,
可以把所有边的边权乘-1,然后用Bellman-Ford算法或SPFA算法求最短路径长度,
将所得结果取反即可。(不能使用Dijkstra,因为可能出现负权边)
2.对一个有正环的图,最长路径是不存在的。(因为一条边,一个顶点可以访问多次)
但是最长简单路径是存在的,只是无法通过Bellman-Ford等算法求解。
原因是最长路径问题是NP-Hard问题(没有多项式时间复杂度算法问题)
三、关键路径
给出比上面求解更快的方法
记录:
活动最早开始时间:e[r]; 活动最迟开始时间:l[r]
关键路径:e[r]==l[r],最早开始时间=最迟开始时间(不能拖延)
->事件最早发生时间:ve[i];事件最迟发生时间:vl[i]
1.活动ar只要在事件vi最早发生时马上开始,就可以使得活动ar的的开始时间最早
e[r]=ve[i]
2.如果l[r]是活动ar的最迟发生时间,那么l[r]+length[r]就是事件vj的最迟发生时间
l[r]=vl[j]-length[r]
将问题由求e[],l[]两个数组(活动)
转化为了
求ve[],vl[]两个数组(事件)
如左图,求ve[j],必须先知道其所有前驱结点ve[i1]-ve[ik]的值
如右图,求vl[i], 必须先知道其所有后继结点vl[j1]-vl[jk]的值
1.为了保证访问ve[j]时,其所有前驱结点都访问完毕——使用拓扑排序
因为通过后继ve[j]找其所有前驱比较麻烦,因此可以直接每次更新ve[i]所有后继结点
2.为了保证访问vl[i] 时,其所有后继结点都访问完毕——使用逆拓扑序列
访问一遍后继结点,通过每个后继结点更新前驱
//拓扑排序,顺便求ve[]
stack<int> topOrder;
bool topologicalSort(){
queue<int> q;
for(int i=0;i<nv;i++){
if(inDegree[i]==0){
q.push(i);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
topOrder.push(u);//将u加入拓扑序列
for(int i=0;i<Adj[u].size();i++){
int v=Adj[u][i].v; //u的i号后继结点编号为v
inDegree[v]--;
if(inDegree[v]==0){
q.push(v);
}
//用ve[u]来更新u的所有后继结点v
if(ve[u]+Adj[u][i].weight>ve[v]){
ve[v]=ve[u]+Adj[u][i].weight;
}
}
}
if(topOrder.size()==nv) return true;
else return false;
}
//逆拓扑序列,并求vl
//逆拓扑序列:颠倒拓扑序列,只需要将topOrder逐个出栈即可
bool reverse_topologicalSort(){
fill(vl,vl+nv,ve[nv-1]);//vl初始化为终点的最早发生时间ve[nv-1](假设终点为nv-1)
//因为终点的ve一定是最大的,且ve[终点]=vl[终点]
while(!topOrder.empty()){
int u=topOrder.top();
topOrder.pop();
for(int i=0;i<Adj[u].size();i++){
int v=Adj[u][i].v;
//用u所有后继结点v的vl值来更新前驱结点vl[u]
if(vl[v]-Adj[u][i].weight<vl[u]){
vl[u]=vl[v]-Adj[u][i].weight;
}
}
}
}
总结找关键路径方法:
int CriticalPath(){
//1.1:计算ve数组
fill(ve,ve+nv,0);//ve数组初始化
if(topologicalSort()==false){
return -1;//不是有向无环图,返回-1
}
//1.2:计算vl数组
fill(vl,vl+nv,ve[nv-1]);//vl数组初始化
while(!topOrder.empty()){
int u=topOrder.top();
topOrder.pop();
for(int i=0;i<Adj[u].size();i++){
int v=Adj[u][i].v;
//用u所有后继结点v的vl值来更新vl[u]
if(vl[v]-Adj[u][i].weight<vl[u]){
vl[u]=vl[v]-Adj[u][i].weight;
}
}
}
//2:遍历所有边,计算e[]和l[]数组
for(int u=0;u<nv;u++){
for(int i=0;i<Adj[u].size();i++){
int v=Adj[u][i].v,w=Adj[u][i].weight;
//活动的最早开始时间e,最迟开始时间l
int e=ve[u],l=vl[v]-w;
if(e==l){
//输出关键活动
printf("%d->%d\n",u,v);
}
}
}
return ve[nv-1];//返回关键路径长度
}
//邻接表存的就是边
1.如果需要存储活动的最早开始时间e和最迟开始时间l,则只需在结构体Node中添加域e和l
2.如果事先不知道终点(汇点)编号,如何初始化vl数组的值呢?
ve数组指事件的最早开始时间,汇点的最早开始时间一定是最大的,因此取ve数组最大值即可。
3.如果需要存储关键路径?
新建一个邻接表,将关键路径的边存进去,最后按需要遍历所有边(DFS,BFS)即可
终点的最早开始时间ve[终点]=最迟开始时间vl[终点]
(最早)工期:
- 单一汇点时,汇点的最早开始时间ve[终点]
- 多汇点时,最大的ve[]