算法背景
拓扑排序处理的是有向无环图,当图中的边被赋予权重时,图就变成了网,若这些带权边表示工程中的某项活动,这个网就称AOV(Adversity on edge)网,与之相对的一个概念是赋予图中顶点以权重,并将顶点视为某项活动,这就构成了AOV(Adversity on vertex)网。AOV网可以容易地转化为AOE网,只要将顶点视为一个带权边,边的两端分别表示活动的开始与结束,边的权重正是原来点的权重,一般表示活动用时,原来网格中的边视为空活动,删去即可,这就将AOV网转化成为了AOE网,所以我们将着重探讨用AOE网模型解决工程最短用时问题。
若要完成某项工程,无疑要完成工程中的所有活动,我们已在AOE网中用边来表示,注意AOE网是从有向无环图演变来的,所以活动与活动之间存在先后关系,那么完成某项活动势必走出一条单向路径,完成工程就会形成多条这样的路径,而工程的耗时正是由这些路径中最长的(代表用时最长)的那条决定的,毕竟可以同时进行多项活动。所以如何求出AOE网中的最长路径,成为算法要解决的关键问题,这最长路径,也被称为该工程的关键路径。
虽然前面已学习了求解图中最短路径的算法,但遗憾的是那些算法,诸如Dijkstra、BF和SPFA均无法应用到求解关键路径的过程中来,原因各异。因而求解关键路径,需要研究出一种全新的算法,这算法以时间为关键词,并引入“事件”这一新概念。其实事件就是由网中的顶点表示的,前面在谈到将AOV转化为AOE网时,有讲过边的两个端点分别表示活动的开始和结束,这正是AOE网中顶点的意义,可以表示其前面连接边活动(如果有的话)的结束,也可以表示其后面连接边活动(如果有的话)的开始。
在上面给出的丑图中,可以看到事件E2前面连着活动A1,后面连着活动A2,要注意到这样一个事实:只有A1结束了,E2才能发生;只有E2发生了,A2才能开始。然而讨论点(事件)的最早发生时间,和最晚发生事件,会比讨论边(活动)的最早结束时间和最晚开始时间来得直观,原因是对于点的时间可以由其后继点递推计算,但对于边的时间,你可以用另一条边递推计算得到吗?答案显然是否定的,但边的时间可以由点的时间确定,下面具体谈谈点与点之间是如何递推的:
1.事件的最早发生时间ve:它的计算基于这样一个事实,只有点的所有前驱点都已发生,该点才能发生。所以若要求某个v结点的ve[v],须保证v的所有前驱结点的ve已计算完成,并在其中找到最大值ve[u](即前驱中最晚发生的),加上u–>v的活动用时,就求出了ve[v]。那么这求解顺序便可以由拓扑排序来保证。
2.事件的最晚发生事件vl:有一个显然的事实,某个点u,即某事件u,虽然可以拖延发生时间,但不能拖延到使得其后继点无法发生,即vl[u]+活动用时,不能超过vl[u的后继],假设这些后继中vl最小的点为v,就有:vl[u]+AOE[u][v]<=vl[v],即vl[u]<=vl[v]-AOE[u][v],vl又代表u的最迟发生时间,于是有vl[u]=vl[v]-AOE[u][v]。所以求解一个点的vl值时,须保证该结点的后继已全部计算完成,求解顺序可以由拓扑排序的逆序列保证,只需要将上面的拓扑排序序列反转一下即可。
求出ve数组和vl数组,在求活动的最早开始时间(记e)和最晚开始时间(记l)就很容易了,其前端事件最早发生时间(ve[u])就是活动的最早开始时间(e),其后端事件的最晚发生时间(vl[v])剪掉活动用时(AOE[u][v]),就是活动最晚开始时间(l)。显然关键路径上所有活动必须紧凑进行,不能拖延,这样才能保证整个工程用时最少对吗?所以有e==l,这就是判断一个活动是否在关键路径上的依据。
至此,算法总体内容就介绍完了,下面上代码。
重要模块
拓扑排序并求解ve
stack<int> topOrder;
bool topologicalSort(){
queue<int> q;
for(int i=0;i<vnum;i++){
//ve初始化为0
ve[i] = 0;
if(inDegree[i]==0){
//每次把入度为0的点加入队列
q.push(i);
}
}
while(!q.empty()){
int u = q.front();
q.pop();
//将队首元素加入拓扑序列
topOrder.push(u);
for(int i=0;i<G[u].size();i++){
//遍历u的后继点,进行入度-1操作
int v = G[u][i].v;
//边的权值
int weight = G[u][i].w;
if(--inDegree[v]==0)
//入度减为0的点入队
q.push(v);
//根据前驱的ve算后继,取最大的
ve[v] = max(ve[v],ve[u] + weight);
}
}
if(topOrder.size()==vnum)
return true;//所有结点都排好序
else
return false;//图中有环
}
逆拓扑排序并求解vl
前面用栈存拓扑序列,弹栈就能得到逆拓扑序列。
void reTopoSort(){
//将vl初始化为终点(这里终点编号为vnum-1)ve值
//终点不存在拖延发生一说,其ve==vl,且即为工程最短用时
fill(vl,vl+vnum,ve[vnum-1]);
while(topOrder.size()!=0){
int u = topOrder.top();
topOrder.pop();
for(int i=0;i<G[u].size();i++){
int v = G[u][i].v;
int weight = G[u][i].w;
//用u的后继更新其vl值,取最小
vl[u] = min(vl[v]-weight,vl[u]);
}
}
}
依据ve、vl确定关键路径
void criticalPath(){
for(int u=0;u<vnum;u++){
for(int i=0;i<G[u].size();i++){
int v = G[u][i].v;
int weight = G[u][i].w;
//求活动最早最迟开始时间e和l
int e = ve[u];
int l = vl[v] - weight;
if(e==l){
//是关键路径上的活动,输出
printf("(%d,%d),",u,v);
}
}
}
}
实际应用
题目
- 输入
第一行输入一个正整数n(1<=n<=5),其代表测试数据数目,即图的数目
第二行输入x(1<=x<=15)代表顶点个数,y(1<=y<=19)代表边的条数
第三行给出图中的顶点集,共x个小写字母表示顶点
接下来每行给出一条边的始点和终点及其权值,用空格相隔,每行代表一条边。- 输出
第一个输出是图的关键路径(用给出的字母表示顶点, 用括号将边括起来,顶点逗号相隔)
第二个输出是关键路径的长度
每个矩阵对应上面两个输出,两个输出在同一行用空格间隔,每个矩阵的输出占一行。
代码
要注意的是题目中源点是要自己再确定的,源点就是ve==0的点。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <stack>
#include <queue>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
const int maxV = 25;
struct node{
int v;
int w;
node() {}
node(int _v,int _w){
v = _v;
w = _w;
}
};
map<char,int> P;//顶点集与编号的映射
char V[maxV];//编号与顶点集的映射
vector<node> G[maxV];//图
stack<int> topOrder;//拓扑序列
int inDegree[maxV];//顶点入度
//ve和vl数组
int ve[maxV];
int vl[maxV];
void init(int x){
//每一轮测试前清空
for(int i=0;i<x;i++)
G[i].clear();
fill(inDegree,inDegree+x,0);
fill(ve,ve+x,0);
P.clear();
}
bool topologicalSort(int x){
queue<int> q;
for(int i=0;i<x;i++){
if(inDegree[i]==0)
q.push(i);
}
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u);
for(int i=0;i<G[u].size();i++){
int v = G[u][i].v;
int weight = G[u][i].w;
if(--inDegree[v]==0)
q.push(v);
ve[v] = max(ve[v],ve[u]+weight);
}
}
if(topOrder.size()==x)
return true;
else
return false;
}
void criticalPath(int x){
if(topologicalSort(x)==false){
puts("ERROR");
return;
}
fill(vl,vl+x,ve[x-1]);
while(topOrder.size()!=0){
int u = topOrder.top();
topOrder.pop();
for(int i=0;i<G[u].size();i++){
int v = G[u][i].v;
int weight = G[u][i].w;
vl[u] = min(vl[u],vl[v]-weight);
}
}
//找关键路径
int next[maxV];
for(int u=0;u<x;u++){
for(int i=0;i<G[u].size();i++){
int v = G[u][i].v;
int weight = G[u][i].w;
int e = ve[u];
int l = vl[v] - weight;
if(e==l)
next[u] = v;
}
}
//找源点
int s;
for(int i=0;i<x;i++){
if(ve[i]==0)
s = i;
}
//输出关键路径
while(s!=x-1){
printf("(%c,%c) ",V[s],V[next[s]]);
s = next[s];
}
//输出工程最短用时
printf("%d\n",ve[x-1]);
}
int main(){
int n,x,y;
cin>>n;
while(n--){
cin>>x>>y;
init(x);
cin>>V;
for(int i=0;i<strlen(V);i++)
//建立字符映射
P[V[i]] = i;
while(y--){
int u,v,w;
char s,t;
cin>>s>>t>>w;
//将顶点转化为编号
u = P[s],v = P[t];
//建立边结构体加入图
node edge(v,w);
G[u].push_back(edge);
//终点v的入度+1
inDegree[v]++;
}
criticalPath(x);
}
}
结果
反思总结
本来没想花这么多时间在AOE上面的,但是我的记忆力可能真的变差了(不知道是不是喝酒喝的T^T),今天发现拿到题目根本无从下手,完全想不起来如何处理,所以又一通啃书,顺便记录一下学习过程。写博客的时候发现自己有好多地方都没搞清楚,彻彻底底梳理了一遍,应该印象会比之前深刻。