基本概念
有向无环图(DAG)
如何用有向无环图(DAG,Directed Acyclic Graph) 来表示偏序关系?
- 设 R R R 是有穷集合 X X X 上的偏序关系,对 X X X 中每个 v v v,用一个以 v v v 为标号的顶点表示,由此构成顶点集 V V V;对任意 ( u , v ) ∈ R , ( u ≠ v ) ( u , v )∈R,( u ≠ v ) (u,v)∈R,(u=v)(严格偏序或反自反偏序关系),由对应两个顶点建立一条有向边,由此构成边集 E E E, 则 G = ( V , E ) G =( V , E ) G=(V,E) 是有向无环图。
拓扑排序
拓扑排序(Topological Sorting):是由某个集合上的一个偏序关系得到该集合上的一个全序的过程,所得到的线性序列称为拓扑序列(Topological order)。
在图论中,拓扑排序是一个有向无环图的所有顶点的线性序列,且该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次;
- 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面;
AOV 网
AOV 网就是一种有向无环图。
一个较大的工程往往被划分成许多子工程,在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的,但有些子工程没有先决条件,可以安排在任何时间开始。
为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,用顶点表示活动,用弧表示活动之间的优先关系,称这样的有向图为顶点表示活动的网,简称 AOV 网。
- AOV 网中的弧表示活动之间存在的某种制约关系。
- 一个 AOV 网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行。
其实 AOV 网就体现了各个子工程之间的一种偏序关系。在 AOV 网中所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列,由AOV网构造拓扑序列的过程叫做拓扑排序。
AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
DAG 拓扑排序算法
课程及课程间的先修关系是偏序关系,可以用 AOV 网(DAG)表示。
利用 AOV 网进行拓扑排序的基本思想:
- (1) 从 AOV 网中选择一个没有前驱的顶点并且输出它;
- (2) 从 AOV 网中删去该顶点和所有以该顶点为尾的弧;
- (3) 重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点;
广搜优先搜索实现拓扑排序
算法过程——可以使用广度优先搜索算法(使用队列):
-
(1)建立入度为零的顶点队列;
-
(2)扫描顶点表,将入度为0的顶点入队(初始化);
-
(3)
while(队列不空)
- (3.1)输出队头结点;
- (3.2)记下输出结点的数目;
- (3.3)删去与之关联的出边;
- (3.4)若有入度为 0 的结点,入队。
-
(4)若输出结点个数小于 n n n,则输出有环路;否则拓扑排序正常结束。
注意事项:
- 若图中还有未输出的顶点,但已跳出循环处理,说明图中还剩下一些顶点,它们的入度都大于 0,也就是都有直接前驱,这时网络中必存在有向环。
与广度优先搜索的区别:
- 搜索起点是入度为 0 的顶点;
- 需判断是否有环路,看最后输出的顶点数与图中顶点数是否相同;
- 需删除邻接于 v 的边(引入数组
indegree[ ]
或在顶点表中增加一个属性域indegree
)
伪代码如下:
void Topologicalsort(AdjGraph G)
{
QUEUE Q ;
count = 0 ;
MAKENUlLL(Q) ;
for(v=1; v<=G.n; ++v)
if(indegree[v] ==0)
ENQUEUE(v, Q) ;
while(!EMPTY(Q)) {
v = FRONT(Q) ;
DEQUEUE(Q) ;
cout << v << " "; // 广度优先搜索中,入队列时访问或出队列时访问是一样的
count ++ ;
for(邻接于 v 的每个顶点 w)
if(!(--indegree[w]))
ENQUEUE(w,Q) ;
}
if(count < n)
cout << “图中有环路” ;
}
深度优先搜索非递归实现拓扑排序
也可以使用深度优先搜索来获取拓扑序列,递归或非递归实现均可。
在深度优先搜索的非递归实现中,需要用到栈:
-
(1)建立入度为零的顶点栈;
-
(2)扫描顶点表,将入度为0的顶点栈;
-
(3)
while(栈不空)
- (3.1)输出队头结点;
- (3.2)记下输出结点的数目;
- (3.3)删去与之关联的出边;
- (3.4)若有入度为0的结点,入栈;
-
(4)若输出结点个数小于 n n n,则输出有环路;否则拓扑排序正常结束。
伪代码如下:
void Topologicalsort(AdjGraph G)
{
STACK S ;
count = 0 ;
MAKENUlLL(S) ;
for(v=1; v<=G.n; ++v)
if(indegree[v] == 0)
PUSH(v, S) ;
while(!EMPTY(S)) {
v = top(S) ;
S.pop() ;
cout << v << " "; // 深度优先搜索中,必须在出栈时访问
count ++ ;
for(邻接于 v 的每个顶点 w)
if(!(--indegree[w]))
PUSH(w,S) ;
}
if(count < n)
cout << “图中有环路” ;
}
深度优先搜索递归实现拓扑排序
算法介绍
参考《算法导论(第三版)》22.4 节。
算法基本思想:递归地对有向无环图(前提)进行深度优先搜索,求每个节点的结束时间(在求强连通分量的 Kosaraju 算法中也需要用到结束时间),然后按照节点的结束时间从大到小排序即可,便可以得到拓扑排序序列。
算法证明
证明该拓扑排序算法 T O P O L O G I C A L − S O R T TOPOLOGICAL-SORT TOPOLOGICAL−SORT 生成的是有向无环图的拓扑排序:
假定在有向无环图 G = ( V , E ) G=(V,E) G=(V,E) 上运行 DFS 来计算节点的完成时间(结束时间)。我们只需要证明,对于任意一对不同的节点 u , v ∈ V u,v\in V u,v∈V,如果图 G G G 中包含一条从节点 u u u 到节点 v v v 的边,则 u u u 的结束时间要比 v v v 的结束时间晚。考虑算法所探索的任意一条边 ( u , v ) (u,v) (u,v),只有两种可能的情况:
- 如果 v v v 还没有被访问过,它将成为 u u u 的后代,因此肯定有 u u u 的结束时间比 v v v 要晚;
- 如果 v v v 已经访问结束了(即 v v v 的所有后继都已经访问完了),此时 v v v 的结束时间已经被设置好了,我们还需要对 u u u 进行探索, u u u 的结束时间还没有被设定。但一旦我们对 u u u 的结束时间进行设定,肯定是比 v v v 的结束时间晚的。
因此,对于任意一条边 ( u , v ) (u,v) (u,v),肯定有 u u u 的结束时间比 v v v 的结束时间晚,那么我们按结束时间从大到小输出,就可以得到有向无环图的拓扑排序。
上图旨在说明,无论每次从哪个顶点开始进行对有向无环图深度优先搜索,然后记录每个节点的结束时间,最后都可以得到正确的拓扑序列(而没有必要每次都从入度为零的节点开始搜)。
如节点 1 1 1 上的 1 / 4 1/4 1/4,表示该节点的开始时间是 1 1 1,结束时间是 4 4 4,按节点的 I D ID ID 执行深度优先搜索(而不是每次都从入度为零的点开始搜),然后按每个节点的结束时间从大到小输出, 9 , 8 , 6 , 7 , 4 , 5 , 3 , 2 , 1 , 10 9,8,6,7,4,5,3,2,1,10 9,8,6,7,4,5,3,2,1,10,这是一个有效的拓扑序列。
伪代码(没有检测是否有环,前提假设是有向无环图):
/* 按弧的正向搜索,起点如何选择无所谓 */
int in_order[MAX_VEX] ; // 记录每个节点的结束时间
bool visited[MAX_VEX] ; // 访问标志位
STACK S; // 存储节点的结束时间,即先结束的节点先入栈
void DFS(OLGraph *G , int v)
{
ArcNode *p;
Count = 0;
visited[v] = TRUE;
for(p=G->xlist[v].firstout; p!=NULL; p=p->tlink)
if(!visited[p->headvex])
DFS(G, p->headvex);
in_order[count++]=v; // 结束时间,即出系统栈的顺序,不是真的深度优先搜索序列
PUSH(S, v) ; // 按节点的结束时间入栈
}
void Topological_sort(OLGraph *G)
{
MAKENULL(S);
for(u=1; u<=n; u++)
visited[u] = FALSE;
for( u=1;u<=n;u++)
if (!visited[u])
DFS(u) ;
for(!S.empty()) { // 依次弹栈,得到的就是拓扑序列
print(TOP(S)) ;
POP(S);
}
}