目录
拓扑排序
如果有一个有向图的任意顶点都无法通过一些有向边回到身边,那么称这个有向图为有向无环图。
拓扑排序是将有向无环图的所有顶点排成一个线性序列,使得对图中的任意两个顶点u,v,如果存在边u->v,那么在序列中u一定在v前面,这个序列被称为拓扑序列。
拓扑排序实现的步骤如下:
(1)定义一个队列Q,并将所以入度为0的结点加入队列。
(2)取队首元素,输出。然后删去所有从它出发的边,并令这些边到达的顶点的入度减1,如果某个顶点的入度减为0,则将其加入队列。
(3)反复进行(2)操作,直到队列为空。如果队列为空时入过队的结点数目恰好为N,说明拓扑排序成功,图为有向无环图,否则,拓扑排序失败,图中有环。
可以用邻接表实现拓扑排序。显然,由于需要记录结点的入度,因此需要额外建立一个数组inDegree[maxn],并在程序一开始读入图时就记录好每个结点的入度。接下来只需要按上面说的步骤实现即可。
vector<int> G[maxn];
int n,m,inDegree[maxn];
bool topologicalSort(){
int num=0;
queue<int> q;
for(int i=0;i<n;i++){
if(inDegree[i]==0){
q.push(i);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
inDegree[v]--;
if(inDegree[i]==0){
q.push(v);
}
}
G[u].clear();
num++;
}
if(num==n){
return true;
}
else{
return false;
}
}
拓扑排序的一个重要的应用就是判断一个给定的图是否是有向无环图。如果有多个入度为0的顶点,选择编号最小的顶点,把么把queue改为priority_queue,并保持队首元素是优先队列的最小的元素即可。
关键路径
AOE网是指用带权的边集表示活动,而用顶点表示时间的有向图。关键路径就是AOE网中的最长路径,而把关键路径上的活动称为关键活动。
(1)计算一个事件的最早发生时间ve数组,可以使用拓扑排序实现,在拓扑排序访问到某个结点时,不是让它去找前驱结点来更新ve[i],而是使用ve[i]去更新其所有后继结点的ve值。
//拓扑排序 ,求ve数组
stack<int> topOrder;
bool topOrdercalsort(){
queue<int> q;
for(int i=0;i<n;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;
inDegree[v]--;
if(inDegree[v]==0){
q.push(v);
}
//用ve[u]来更新u的所有后继结点
if(ve[u]+G[u][i].w>ve[v]){
ve[v]=ve[u]+G[u][i].w;
}
}
}
if(topOrder.size()==n){
return true;
}
else{
return false;
}
}
(2)计算一个事件的最晚发生时间vl数组,可以通过逆拓扑排序来实现。可以通过颠倒拓扑排序来得到一组合法的逆拓扑排序。此时会发现,在上面实现拓扑排序时使用了栈来存储拓扑序列,那么只需要按顺序出栈就是逆拓扑序列。
fill(vl,vl+n,ve[n-1]);
while(!topOrder.empty()){
int u=topOrder.top();
topOrder.pop();
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v;
if(vl[v]-G[u][i].w<vl[u]){
vl[u]=vl[v]-G[u][i].w;
}
}
}
(3)先求点,再夹边。下面是主体部分的代码。(适用汇点确定且唯一的情况)
//关键路径,不是有向无环图返回-1,否则返回关键路径长度
int CriticalPath(){
memset(ve,0,sizeof(ve));
if(topOrdercalsort()==false){
return -1;
}
fill(vl,vl+n,ve[n-1]);
//直接使用topOrder出栈即为逆拓扑序列,求解vl数组
while(!topOrder.empty()){
int u=topOrder.top();
topOrder.pop();
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v;
if(vl[v]-G[u][i].w<vl[u]){
vl[u]=vl[v]-G[u][i].w;
}
}
}
//遍历邻接表的所有边,计算活动的最早开始时间e和最迟开始时间l
for(int u=0;u<n;u++){
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v,w=G[u][i].w;
int e=ve[u],l=vl[v]-w;
if(e==l){
printf("%d->%d\n",u,v);
}
}
}
return ve[n-1];
}
在上面的代码中没有将活动的最早开始时间e和最晚开始时间l存储下来,因为一般来说e和l只是用来判断当前活动是否存在关键活动,没有必要单独存下来。如果确实想要存下来,只需要在结构体node中添加域e和l即可。
如果事先不知道汇点编号,最快的办法获得关键路径长度就是取ve数组的最大值。因为ve数组的含义是事件的最早开始时间,因此所有事件中ve最大的一定是最后一个事件,也就是汇点。于是只需要在fill函数之前添加一小段语句,然后改变vl函数初始值即可。
int maxLength=0;
for(int i=0;i<n;i++){
if(ve[i]>maxLength){
maxLength=ve[i];
}
}
fill(vl,vl+n,maxLength);
即便图中有多条关键路径,但是如果题目只要求输出关键活动,按照上面的写法就可以。如果要求输出所有的关键路径,就需要把关键活动存下来,方法就是新建一个邻接表,当确定边u->v是关键活动时,就将它加入邻接表。这样最后生成的邻接表就是所有关键路径合成的图,可以用dfs遍历来获取所有关键路径。