拓扑排序与关键路径

前:数据结构学习关键路径,想到以前写过拓扑排序,这里再来重新写一下拓扑排序与关键路径

一.拓扑排序

<1>概念:对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。
拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。

!!即拓扑排序后的任务序列完全满足任务的最优先后性!!(基于图联系)

如图所示就是一个简单的AOV网,任务3要想执行,必须等任务1和任务2都执行完才能执行。所以上图的拓扑序列:1234或2134

   注:AOV网只能反映活动之间的先后关系以及优先顺序。

<2>拓扑排序的实现步骤:(无先驱者优先!)

在有向图中选一个没有前驱的顶点并且输出
从图中删除该顶点和所有以它为尾的弧(白话就是:删除所有和它有关的边)
重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。


<3>拓扑排序的两种实现算法之一:Kahn算法(从度出发,依照队列进行)

核心思想:以度为入手点,度为零的节点即为无前驱点,找出最早一批无前驱点,不断删除节点和相关边,来判断相邻节点是否入队(度为零无前驱入队)
 

#include <iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=101;
int G[maxn][maxn];//节点是否联通
int flow[maxn];//记录节点的入度
int n,m;
vector<int> num;//保存结果顺序
void dfs()
{
    int counter=0;//用来判断有无环路!
    queue<int> q;
    for(int i=1; i<=n; i++)
    {
        if(flow[i]==0)//最开始从入度为零出发,找出第一批无前驱点放入队列
            q.push(i);
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        num.push_back(u);
        for(int j=1; j<=n; j++)
        {
            if(G[u][j]&&)
            {
                flow[j]--;
                if(flow[j]==0)//如果入度为零了,也要存入queue
                    q.push(j);
            }
        }
        ++counter;
    }
    cout<<counter<<endl;
    if(counter!=n)//不=节点个数即有环路,一般来说大于n!
        cout<<"´æÔÚ»·£¡"<<endl;
}
int main()
{
    while(cin>>n>>m&&(n||m))
    {
        num.clear();
        memset(G,0,sizeof(G));
        memset(flow,0,sizeof(flow));
        while(m--)
        {
            int a,b;
            cin>>a>>b;
            G[a][b]=1;
            flow[b]++;//入度增加
        }
        dfs();
        for(int i=0; i<num.size(); i++)
        {
            if(i==0)
                cout<<num[i];
            else
                cout<<" "<<num[i];
        }
        cout<<endl;
    }
    return 0;
}

<4>拓扑排序的两种实现算法之二:DFS算法(基于递归与数组标记)


核心思路:用vis数组来标记节点状态:0表示未访问,1表示已经访问,-1表示正在访问。如某节点递归过程中发现子节点状态为-1,则说明图中有环!!

#include <iostream>
#include<cstring>
#include<cstdio>
#include<stack>
using namespace std;
const int maxn=1000;
int G[maxn][maxn];
int vis[maxn];
int n,m;
stack<int> num;
bool dfs(int a)
{
    vis[a]=-1;
    for(int j=1;j<=n;j++)
    {
        if(G[a][j])
        {
            if(vis[j]==-1)//环
                return false;
            else if(!vis[j]&&!dfs(j))//若未访问过且访问后是-1还是有环
                return false;
        }
    }
    vis[a]=1;//标记已经访问
    num.push(a);//栈压入元素放在末尾!由于加入顶点到集合中的时机是在dfs方法即将退出之时,(若用数组,则是num[--t]=a;t=n;)
        //而dfs方法本身是个递归方法,
        //仅仅要当前顶点还存在边指向其他不论什么顶点,
        //它就会递归调用dfs方法,而不会退出。
        //因此,退出dfs方法,意味着当前顶点没有指向其他顶点的边了
        //,即当前顶点是一条路径上的最后一个顶点。
        //换句话说其实就是此时该顶点出度为0了
    return true;
}
int main()
{
    while(cin>>n>>m)
    {
        memset(G,0,sizeof(G));
        memset(vis,0,sizeof(vis));
        for(int i=0;i<m;i++)
        {
            int a,b;
            cin>>a>>b;
            G[a][b]=1;
        }
        bool sign=true;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i])
            {
                if(!dfs(i))
                {
                    sign=false;
                    cout<<"there is a huan!\n";
                    break;
                }
            }
        }
        if(sign)
        {
            while(!num.empty())
            {
                cout<<num.top()<<" ";
                num.pop();
            }
            cout<<endl;
        }
    }
    return 0;
}

二.关键路径

1.什么是AOE网:前面我们学习了AOV网,它表示事件的先后关系。在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网。

显然:AOE网只是比AOV网多了一个边权,这里的顶点表示事件也就是事物的状态,有向边表示从一个状态过渡到另一个状态的活动所需要的时间。可见,AOE网的研究问题为:

(1)整个工程是否可行:是否存在关系环

(2)整个工程的各个活动先后关系:基于拓扑排序

(3)整个工程的最早完成时间:关键路径时间

2.AOE网名词及其解释

(1)关键路径:AOE-网中,从起点到终点 最长 的路径的长度(长度指的是路径上边的权重和),关键路径长度是整个工程所需的最短工期。

解释:关键路径是整个工程中最关键的一个活动路径,何为关键,也就是这条路径上不论哪一部分活动的改变都会影响整个工程完成时间。这里为什么是最长的长度呢?可以参看上图:

整个工程完成的最短时间为 v(1,3) + v(3,4) = 16; 如果我们改变v(2,3)为4,对整个工程并没有什么影响。但是如果我们改变v(1,3) = 7,那么整个工程都会推迟1完成。所以说,整个工程的最长时间恰恰是完成它的最短必要时间。

注:关键路径可能不止一条!

(2)关键活动:最早开始时间 = 最晚开始时间的活动,关键路径上的活动

定义ve[ i ] :顶点 vi的最早发生时间,从起点到vi的最长路径的长度为vi的最早发生时间(等vi之前的所有任务都完成,vi才能开始活动,所以取最长为最早)。同时,vi的最早发生时间也是所有以vi为尾的弧所表示的活动的最早开始时间,使用e(i)表示活动ai最早发生时间。

定义vl[ i ] : 顶点vi的最迟发生时间,在不推迟工期完成的总时间的前提下,可以推迟以vi[ i ]开头的活动的最迟开始时间。从结束点逆拓扑排序,取最小 vl[ k ] - edge.time 

关键活动: 不可推迟的活动,也就是必须准时执行的活动 即 ve[ i ] = vl[ i ]

3.求关键路径以及关键活动

(1)最早开始时间以及关键路径 :基于拓扑排序,从各起点开始正向进行,公式(假设起点为s,终点为e):

  • ve[ s ] = 0
  • ve[ i ] = max(ve[ i ] , ve[ j ] + edge.time);( j为i的前驱点之一)

其中ve[ i ] 为各点的最早开始时间,ve[ e ] = 关键路径长度 = 工程最短工期

(2)最晚开始时间(在已知(1)的前提下):基于逆拓扑排序

  • vl[ e ] = ve[ e ]
  • vl[ i ] = min(vl[ i ] , vl[ k ] - edge.time)( k 为 i 的后继点之一)

(3)关键活动:所有ve[ i ] = vl [ i ] 的活动

4.代码:基于多个起点多个终点

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 100 + 7;
struct Edge{
    int from,to,next,time;
}edge[maxn*1000],edge2[maxn*1000];
int n,m,tot,tot2,head[maxn],head2[maxn];
int in[maxn],out[maxn],ve[maxn],vl[maxn],Max;
void addEdge(int a,int b,int c,int d){//正向建边用于拓扑排序求最早开始时间
    edge[tot].from = a;edge[tot].to = b;edge[tot].time = c;edge[tot].next = head[a],head[a] = tot++;
}
void addEdge2(int a,int b,int c){//反向建边用于逆拓扑排序求最晚开始时间
    edge2[tot2].from = a;edge2[tot2].to = b;edge2[tot2].time = c;edge2[tot2].next = head2[a],head2[a] = tot2++;
}
struct Node{//存关键活动
   int s,e;
   bool operator <(const Node &another)const{//起点小先输出
       return s > another.s;
   }
   Node(int a,int b):s(a),e(b) {}
};
bool TupoSort(){//拓扑排序
   queue<int> que;
   memset(ve,0,sizeof(ve));
   for(int i = 1;i<=n;i++){
      if(in[i]==0)que.push(i);
   }
   int cnt = 0;
   while(!que.empty()){
      cnt++;
      int p = que.front();
      que.pop();
      for(int i = head[p];~i;i = edge[i].next){
          in[edge[i].to]--;
          if(ve[edge[i].to] < ve[p] + edge[i].time){
              ve[edge[i].to] = ve[p] + edge[i].time;
          }
          if(!in[edge[i].to])que.push(edge[i].to);
      }
   }
   if(cnt!=n)return false;//有环
   return true;
}
void ReTupoSort(){//逆拓扑排序
   memset(vl,INF,sizeof(vl));
   queue<int> que;
   for(int i = 1;i<=n;i++){
      if(out[i]==0){
        que.push(i);
        vl[i] = Max;//注意这里!!都要更新为最早工期时间而不是ve[i]
      }
   }
   while(!que.empty()){
      int p = que.front();
      que.pop();
      for(int i = head2[p];~i;i = edge2[i].next){
         out[edge2[i].to]--;
         if(vl[edge2[i].to] > vl[p] - edge2[i].time){
            vl[edge2[i].to] = vl[p] - edge2[i].time;
         }
         if(!out[edge2[i].to])que.push(edge2[i].to);
      }
   }
}
int main()
{
    tot = tot2 = 0;
    memset(head,-1,sizeof(head));
    memset(head2,-1,sizeof(head2));
    memset(in,0,sizeof(in));
    memset(out,0,sizeof(out));
    scanf("%d%d",&n,&m);
    for(int i = 0;i<m;i++){
        int a,b,v;
        scanf("%d%d%d",&a,&b,&v);
        addEdge(a,b,v);
        addEdge2(b,a,v);
        in[b]++;
        out[a]++;
    }
    if(!TupoSort())printf("0\n");
    else{
       Max = -1;
       for(int i = 1;i<=n;i++){
          if(out[i]==0)Max = max(Max,ve[i]);//取所有结束点里最大的为工程最短时间(都完成)
       }
       printf("%d\n",Max);//输出最短时间
       ReTupoSort();//基于最早开始时间逆拓扑排序
       priority_queue<Node> que;
       for(int i = 1;i<=n;i++){
          if(ve[i]!=vl[i])continue;//非关键活动
          for(int j = head[i];~j;j = edge[j].next){
             if(ve[i]==vl[edge[j].to] - edge[j].time){//找从该点出发的哪个活动是关键活动
                que.push(Node(i,edge[j].to));
             }
          }
       }
       while(!que.empty()){//输出
           Node node = que.top();
           que.pop();
           printf("%d->%d\n",node.s,node.e);
       }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_40772692/article/details/84190698