【图论】 拓扑排序与关键路径

目录

一. 拓扑排序

1. 基本概念

2. 实现步骤

3. 实现算法

3.1 Kahn算法

3.2 DFS算法

二. 关键路径

1. AOE网

2. 名词解释

2.1 关键路径

2.2 关键活动

3. 求解思路

4. 代码实现


一. 拓扑排序

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. 实现步骤

        求解拓扑序列的核心是无先驱者优先。其基本求解思路如下:

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

3. 实现算法

3.1 Kahn算法

        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;
}

3.2 DFS算法

        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. 名词解释

2.1 关键路径

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

        关键路径是整个工程中最关键的一个活动路径,何为关键,也就是这条路径上不论哪一部分活动的改变都会影响整个工程完成时间。这里为什么是最长的长度呢?可以参看上图:整个工程完成的最短时间为 v(1,3) + v(3,4) = 16; 如果我们改变v(2,3)为4,对整个工程并没有什么影响。但是如果我们改变v(1,3) = 7,那么整个工程都会推迟1完成。所以说,整个工程的最长时间恰恰是完成它的最短必要时间。注意:关键路径可能不止一条!

2.2 关键活动

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

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

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

(3)关键活动: 不可推迟的活动,也就是必须准时执行的活动 即 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);
       }
    }
}
拓扑排序关键路径算法是一种用于确定有向无环图中关键路径算法。其基本思想是通过拓扑排序来确定每个节点的最早开始时间和最晚开始时间,从而计算出每个活动的最早开始时间、最晚开始时间和总时差,从而确定关键路径算法流程如下: 1. 对有向无环图进行拓扑排序,得到每个节点的最早开始时间。 2. 从起点开始,按照拓扑序列依次计算每个节点的最晚开始时间。 3. 计算每个活动的最早开始时间、最晚开始时间和总时差。 4. 根据总时差为0的活动确定关键路径。 下面是一个简单的例子,假设有以下有向无环图: ``` A --> B --> C --> D \ / --> E --> ``` 其中,A、B、C、D、E分别表示节点,箭头表示有向边。假设每个节点的执行时间如下: ``` A: 2 B: 3 C: 4 D: 2 E: 1 ``` 则按照上述算法流程,可以得到以下结果: 1. 进行拓扑排序,得到拓扑序列为A -> E -> B -> C -> D。 2. 从起点A开始,按照拓扑序列依次计算每个节点的最晚开始时间,得到以下结果: - A: 最晚开始时间为2 - E: 最晚开始时间为2 - B: 最晚开始时间为5 - C: 最晚开始时间为9 - D: 最晚开始时间为11 3. 计算每个活动的最早开始时间、最晚开始时间和总时差,得到以下结果: - A -> B: 最早开始时间为2,最晚开始时间为5,总时差为3 - A -> E: 最早开始时间为2,最晚开始时间为2,总时差为0 - B -> C: 最早开始时间为5,最晚开始时间为9,总时差为4 - C -> D: 最早开始时间为9,最晚开始时间为11,总时差为2 - E -> B: 最早开始时间为2,最晚开始时间为5,总时差为3 - E -> C: 最早开始时间为3,最晚开始时间为6,总时差为3 4. 根据总时差为0的活动确定关键路径,即A -> E。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿阿阿安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值