拓扑排序和关键路径

一、拓扑排序

什么是拓扑排序?简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

回顾离散数学中关于偏序和全序的定义:

若集合X上的关系R是自反的,反对称的和传递的,则称R是集合X上的偏序关系。

设R是集合X上的偏序,如果对每个x,y∈X必有xRy或yRx,则称R是集合X上的全序关系。

直观地看,偏序指集合中仅有部分成员之间可比较,而全序指集合中全体成员之间均可比较下图所示的两个有向图,图中弧(x,y)表示x≤y,则(a)表示偏序,(b)表示全序。若在(a)的有向图上人为地加一个表示v2≤v3的弧(符号“≤”表示v2领先于v3),则(a)表示的亦为全序,且这个全序称为拓扑有序(Topological Order),而由偏序定义得到拓扑有序的操作便是拓扑排序。

image

如何进行拓扑排序?

  一、从有向图中选取一个没有前驱的顶点,并输出之;
  二、从有向图中删去此顶点以及所有以它为尾的弧;
  重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。没有前驱 — 入度为零,删除顶点及以它为尾的弧– 弧头顶点的入度减1。

image

AOV-网及其拓扑有序序列产生的过程
(a)AOV-网;(b)输出v6之后;(c)输出v1之后;(d)输出v4之后;(e)输出v3之后;(f)输出v2之后

最后得到该有向图的拓扑有序序列为:

v6- v1- v4- v3- v2- v5

 

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_VERTEX_NUM 20 //图的最大顶点数
#define MAX 30       //栈的最大容量
enum BOOL {False,True};
typedef struct ArcNode
{
    int adjvex;              //该弧所指向的顶点的位置
    struct ArcNode *nextarc; //指向下一条弧的指针
} ArcNode;      //弧结点
typedef struct
{
    int indegree[MAX_VERTEX_NUM]; //存放各顶点的入度
    ArcNode* AdjList[MAX_VERTEX_NUM]; //指向第一条依附该顶点的弧的指针
    int vexnum,arcnum;                //图的当前顶点和弧数
} Graph;
typedef struct    //定义堆栈结构
{
    int elem[MAX];   //栈区
    int top;         //栈顶指针
} Stack;
void CreateGraph ( Graph & ); //生成图的邻接表
BOOL TopologicalSort ( Graph );  //进行拓扑排序
void FindInDegree ( Graph& ); //求图各顶点的入度
void Initial ( Stack & );  //初始化一个堆栈
BOOL Push ( Stack &,int ); //将一个元素入栈
BOOL Pop ( Stack&,int & ); //将一个元素出栈
BOOL StackEmpty ( Stack ); //判断堆栈是否为空
void main()
{
    Graph G;  //采用邻接表结构的图
    char j='y';
    BOOL temp;
    while ( j!='N'&&j!='n' )
    {
        CreateGraph ( G );       //生成邻接表结构的图
        temp=TopologicalSort ( G ); //进行拓扑排序
        if ( temp==False ) printf ( "该图有回路!\n" );
        //若返回为False,表明该图存在有环路
        else printf ( "该图没有回路!\n" );
        printf ( "拓扑排序完毕,继续进行吗?(Y/N)" );
        scanf ( " %c",&j );
    }
}

void CreateGraph ( Graph &G )
{//构造邻接表结构的图G
    int i;
    int start,end;
    ArcNode *s;
    printf ( "请输入图的顶点数和弧数:" );
    scanf ( "%d,%d",&G.vexnum,&G.arcnum ); //输入图的顶点数和弧数
    for ( i=1; i<=G.vexnum; i++ ) G.AdjList[i]=NULL; //初始化指针数组
    printf ( "请输入各弧, 格式:弧尾,弧头\n" );
    for ( i=1; i<=G.arcnum; i++ )
    {
        scanf ( "%d,%d",&start,&end ); //输入弧的起点和终点
        s= ( ArcNode * ) malloc ( sizeof ( ArcNode ) ); //生成一个弧结点
        s->nextarc=G.AdjList[start]; //插入到邻接表中
        s->adjvex=end;
        G.AdjList[start]=s;
    }
}
BOOL TopologicalSort ( Graph G )
{//对图G进行拓扑排序,若G存在回路,返回False,否则返回True
    int i,k;
    int count; //计数器
    ArcNode* p;
    Stack S;
    FindInDegree ( G ); //求出图中各顶点的入度
    Initial ( S );   //堆栈初始化,存放入度为零的顶点
    for ( i=1; i<=G.vexnum; i++ )
        if ( !G.indegree[i] ) Push ( S,i ); //入度为零的顶点入栈
    count=0;     //对输出顶点记数
    printf ( "拓扑排序:" );
    while ( !StackEmpty ( S ) )
    {
        Pop ( S,i );  //输出i号顶点并记数
        printf ( "%d->",i );
        count++;
        for ( p=G.AdjList[i]; p; p=p->nextarc )
        {
            k=p->adjvex;       //对i号顶点的每个邻接顶点的入度减一
            if ( ! ( --G.indegree[k] ) ) Push ( S,k ); //若入度为零,入栈
        }
    }
    printf ( "\b\b  \n" );
    if ( count<G.vexnum ) return False; //如输出顶点数少于图中顶点数,则该图有回路
    else return True;
}
void FindInDegree ( Graph &G )
{//求出图G的各顶点的入度,存放在G.indegree[1..G.vexnum]中
    int i;
    ArcNode* p;
    for ( i=1; i<=G.vexnum; i++ )
        G.indegree[i]=0;
    for ( i=1; i<=G.vexnum; i++ )
    {
        for ( p=G.AdjList[i]; p; p=p->nextarc )
            G.indegree[p->adjvex]++;  //弧头顶点的入度加一
    }
}
void Initial ( Stack &S )
{
    S.top=-1;   //栈顶指针初始化为-1
}

BOOL Push ( Stack &S,int ch )
{//将元素ch入栈,成功返回True,失败返回False
    if ( S.top>=MAX-1 ) return False;//判断是否栈满
    else
    {
        S.top++;               //栈顶指针top加一
        S.elem[S.top]=ch;      //入栈
        return True;
    }
}

BOOL Pop ( Stack &S,int &ch )
{//将栈顶元素出栈,成功返回True,并用ch返回该元素值,失败返回False
    if ( S.top<=-1 ) return False;//判断是否栈空
    else
    {
        S.top--;                                //栈顶指针减一
        ch=S.elem[S.top+1];
        return True;
    }
}
BOOL StackEmpty ( Stack S )
{//判断堆栈是否已空,若空返回True,不空返回False
    if ( S.top<=-1 ) return True;
    else return False;
}

运行结果:

image

二、关键路径

重要概念:

AOE (Activity On Edges)网络 如果在无有向环的带权有向图中用有向边表示一个工程中的各项活动(Activity),用边上的权值表示活动的持续时间(Duration),用顶点表示事件(Event),则这样的有向图叫做用边表示活动的网络,简称AOE (Activity On Edges)网络。AOE网是一个带权的有向无环图。

AOE网络在某些工程估算方面非常有用。例如,可以使人们了解:

    a、完成整个工程至少需要多少时间(假设网络中没有环)?

    b、为缩短完成工程所需的时间, 应当加快哪些活动?

关键路径(Critical Path) 在AOE网络中, 有些活动顺序进行,有些活动并行进行。从源点到各个顶点,以至从源点到汇点的有向路径可能不止一条。这些路径的长度也可能不同。完成不同路径的活动所需的时间虽然不同,但只有各条路径上所有活动都完成了,整个工程才算完成。因此,完成整个工程所需的时间取决于从源点到汇点的最长路径长度,即在这条路径上所有活动的持续时间之和。这条路径长度最长的路径就叫做关键路径(Critical Path)。

如图1就是一个AOE网,该网中有11个活动和9个事件。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。如事件v5表示a4和a5活动已经完成,a7和a8活动可以开始。每个弧上的权值表示完成相应活动所需要的时间,如完成活动a1需要6天,a8需要7天。

14、求关键路径 - 墨涵 - 墨涵天地

AOE网常用于表示工程的计划或进度。由于实际工程只有一个开始点和一个结束点,因此AOE网存在唯一的入度为0的开始点(又称源点)和唯一的出度为0的结束点(又称汇点),例如图1的AOE网从事件v1开始,以事件v9结束。同时AOE网应当是无环的。

那么该工程待研究的问题是:1.完成整项工程至少需要多少时间?2.哪些活动是影响工程进度的关键?
由于在AOE-网中有些活动可以并行进行,所以完成工程的最短时间是从开始点到完成点的最长路径的长度(这里所说的路径长度是指路径上各活动持续时间之和,不是路径上弧的数目)。路径长度最长的路径叫做关键路径(Critical path)。
假设开始点是v1,从v1到vi的最长路径叫做时间vi的最早发生时间。这个时间决定了所有以vi为尾的弧所表示的活动的最早开始时间。我们用e(i)表示活动ai的最早开始时间。还可以定义一个活动开始的最迟时间l(i),这是在不推迟整个工程完成的前提下,活动ai最迟必须开始进行的时间。两者之差l(i)-e(i)意味着完成活动ai的时间余量。当这个时间余量等于0的时候,也即是l(i)=e(i)的活动,我们称其为关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程的进度。
因此,分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的功效,缩短整个工期。

 

如何实现关键路径?

由上面的分析可知,辨别关键活动就是要找e(i)=l(i)的活动。为了求得e(i)和l(i),首先应求得事件的最早发生时间ve(j)和最迟发生时间vl(j)。如果活动ai由弧<j,k>表示,其持续时间记为dut(<j,k>),则有如下关系
e(i) = ve(j)
l(i) = vl(k) – dut(<j,k>)
求解ve(j)和vl(j)需分两个步进行:
1) 从ve(0)=0开始向前推进求得ve(j)
Ve(j) = Max{ve(i) + dut(<i,j>) };<i,j>属于T,j=1,2…,n-1
其中T是所有以第j个顶点为头的弧的集合。

2) 从vl(n-1) = ve(n-1)起向后推进求得vl(j)
vl(i) = Min{vl(j) – dut(<i,j>};<i,j>属于S,i=n-2,…,0
其中,S是所有以第i个顶点为尾的弧的集合。
这两个递推公式的计算必须分别在拓扑有序和逆拓扑有序的前提先进行。也就是说,ve(j-1)必须在vj的所有前驱的最早发生时间求得之后才能确定,而vl(j-1)必须在Vj的所有后继的最迟发生时间求得之后才能确定。因此可以在拓扑排序的基础上计算ve(j-1)和vl(j-1)。

 

具体算法描述如下:
1. 输入e条弧<j,k>,建立AOE-网的存储结构
2. 拓扑排序,并求得ve[]。从源点V0出发,令ve[0]=0,按拓扑有序求其余各顶点的最早发生时间ve[i]。如果得到的拓扑有序序列中顶点个数小于网中顶点数n,则说明网中存在环,不能求关键路径,算法终止;否则执行步骤3。
3. 拓扑逆序,求得vl[]。从汇点Vn出发,令vl[n-1] = ve[n-1],按逆拓扑有序求其余各顶点的最迟发生时间vl[i](n-2≥i≥2)。
4. 求得关键路径。根据各顶点的ve和vl值,求每条弧s的最早开始时间e(s)和最迟开始时间l(s)。若某条弧满足条件e(s) = l(s),则为关键活动。

为了能按逆序拓扑有序序列的顺序计算各个顶点的vl值,需记下在拓扑排序的过程中求得的拓扑有序序列,这就需要在拓扑排序算法中,增设一个栈,以记录拓扑有序序列,则在计算求得各顶点的ve值之后,从栈顶到栈底便为逆拓扑有序序列。

 

 

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#include <iostream>
#include <fstream>
#include <queue>
#include <stack>
#include <Windows.h>

using namespace std;

#define INFINITY INT_MAX
#define MAX_VERTEX_NUM 20  //顶点最多个数
#define LENGTH 5           //顶点字符长度

//邻接矩阵
typedef struct _Graph
{
    int matrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];  //邻接矩阵
    char vexs[MAX_VERTEX_NUM][LENGTH];           //顶点数组
    int vexnum;                                  //顶点个数
    int arcs;                                    //弧的个数
}Graph;

int LocateVex(const Graph & g, char name[LENGTH])
{
    for (int i = 0; i < g.vexnum; i++)
    {
        if (0 == strcmp(g.vexs[i], name))
        {
            return i;
        }
    }
    return -1;
}

//图的建造
void CreateGraph(Graph &g)
{
    int i = 0,j = 0;
    ifstream fcin("criticalpath.txt");
    fcin>>g.vexnum;
    for (i = 0; i < g.vexnum; i++)
    {
        for (j = 0; j < g.vexnum; j++)
        {
            g.matrix[i][j] = INFINITY;
        }
    }
    for (i = 0; i < g.vexnum; i++)
    {
        fcin>>g.vexs[i];
    }
    fcin>>g.arcs;
    char arcHead[LENGTH];
    char arcTail[LENGTH];
    int weight;
    for (i = 0; i < g.arcs; i++)
    {
        memset(arcHead, 0, LENGTH);
        memset(arcTail, 0, LENGTH);
        fcin>>arcTail>>arcHead>>weight;
        int x = LocateVex(g, arcHead);
        int y = LocateVex(g, arcTail);
        g.matrix[y][x] = weight;
    }
}

//v的第一个邻接点
int FirstAdjVex(const Graph &g, int v)
{
    for (int i = 0; i < g.vexnum; i++)
    {
        if (g.matrix[v][i] != INFINITY)
        {
            return i;
        }
    }
    return -1;
}

//v相对于w的下一个邻接点
int NextAdjVex(const Graph &g, int v, int w)
{
    for (int i = w + 1; i < g.vexnum; i++)
    {
        if (g.matrix[v][i] != INFINITY)
        {
            return i;
        }
    }
    return -1;
}

//邻接矩阵的输出
void PrintAdjVex(const Graph &g)
{
    for (int i = 0; i < g.vexnum; i++)
    {
        for (int j = 0; j < g.vexnum; j++)
        {
            if (g.matrix[i][j] == INFINITY)
            {
                cout<<"*"<<'\t';
            }
            else
            {
                cout<<g.matrix[i][j]<<'\t';
            }
        }
        cout<<endl;
    }
    cout<<endl;
}

//************************************关键路径************************************begin
int ve[MAX_VERTEX_NUM] = {0};
int vl[MAX_VERTEX_NUM] = {INFINITY};
//查找入度为0的顶点
void FindInDegree(Graph g, int indegree[])
{
    for (int i = 0; i < g.vexnum; i++)
    {
        for (int j = 0; j < g.vexnum; j++)
        {
            if (g.matrix[i][j] != INFINITY)
            {
                indegree[j]++;
            }
        }
    }
}

//拓扑排序
bool TopologicalSort(Graph g, stack<int> &s)
{
    int indegree[MAX_VERTEX_NUM] = {0};
    FindInDegree(g, indegree);
    queue<int> q;
    int i = 0;
    for (; i < g.vexnum; i++)
    {
        if (indegree[i] == 0)
        {
            q.push(i);
        }
    }
    int count = 0;
    while (!q.empty())
    {
        i = q.front();
        s.push(i);
        q.pop();
        count++;
        for (int j = 0; j < g.vexnum; j++)
        {
            if (g.matrix[i][j] != INFINITY)
            {
                if (!(--indegree[j]))
                {
                    q.push(j);
                }
                if (ve[i] + g.matrix[i][j] > ve[j])
                {
                    ve[j] = ve[i] + g.matrix[i][j];
                }
            }
        }
    }
    if (count < g.vexnum)
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool CriticalPath(Graph g)
{
    int i = 0, j =0;
    stack<int> s;
    if (!TopologicalSort(g, s))
    {
        return false;
    }
    for (i = 0; i < g.vexnum; i++)
    {
        vl[i] = INFINITY;
    }
    vl[g.vexnum - 1] = ve[g.vexnum - 1];
   
    while (!s.empty())
    {
        i = s.top();
        s.pop();
        for (j = 0; j < g.vexnum; j++)
        {
            if (g.matrix[i][j] != INFINITY)
            {
                int tmp = g.matrix[i][j];
                if (vl[j] - g.matrix[i][j] < vl[i])
                {
                    vl[i] = vl[j] - g.matrix[i][j];
                }
            }
        }
    }
   
    for (i = 0; i < g.vexnum; i++)
    {
        for (j = 0; j < g.vexnum; j++)
        {
            if (g.matrix[i][j] != INFINITY)
            {
                if (ve[i] == vl[j] - g.matrix[i][j])
                {
                    cout<<g.vexs[i]<<"  "<<g.vexs[j]<<"  "<< g.matrix[i][j]<<endl;
                }
            }
       
        }
       
    }
    cout<<"最长路径需要"<<vl[g.vexnum - 1]<<"天"<<endl;
    return true;
}

//************************************关键路径************************************end

//辅助函数,设置控制台的颜色
void SetConsoleTextColor(WORD dwColor)
{
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
    if (INVALID_HANDLE_VALUE == handle)
    {
        return;
    }
    SetConsoleTextAttribute(handle, dwColor);
}

int main(int argc, CHAR* argv[])
{
    Graph graph;
    CreateGraph(graph);
    SetConsoleTextColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY);
    cout<<"************************邻接矩阵**************************"<<endl;
    PrintAdjVex(graph);
    SetConsoleTextColor(FOREGROUND_GREEN | FOREGROUND_INTENSITY);
    cout<<"************************关键路径**************************"<<endl<<endl;
    CriticalPath(graph);
    cout<<endl<<endl;

    return 0;
}

criticalpath.txt文件内容

9
V1 V2 V3 V4 V5 V6 V7 V8 V9
11
V1 V2 6
V1 V3 4
V1 V4 5
V2 V5 1
V3 V5 1
V4 V6 2
V5 V7 9
V5 V8 7
V6 V8 4
V7 V9 2
V8 V9 4

运行结果:

image

本文链接:http://www.alonemonkey.com/topologicalsort-criticalpath.html