7.5有向无环图应用之拓扑排序
有向无环图(Directed Acyclic Graph)是指一个无环的有向图,简称 DAG。有 向无环图可用来描述工程或系统的进行过程,如一个工程的施工图、学生课程间的制约关系图等。
拓扑排序
用顶点表示活动,用弧表示活动间的优先关系的有向无环图,称为顶点表示 活动的网(Activity On Vertex Network), 简称为 AOV-网。
例如:计算机系学生的一些必修课程及其先修课程的关系如下:
用顶点表示课程,弧表示先决条件,则上述关系可用一个有向无环图表示, 见下图
在有向图 G=(V,{E})中, V 中顶点的线性序列(vi1,vi1,vi3,…,vin)称为 拓扑序列。如果此序列满足条件:对序列中任意两个顶点 vi、vj,在 G 中有一条从vi到vj的路径,则在序列中vi必排在vj之前。
例如,上图的一个拓扑序列为:
AOV-网的特性如下:
- 若 vi为 vj的先行活动,vj为 vk的先行活动,则 vi必为 vk的先行活动,即先 行关系具有可传递性。从离散数学的观点来看,若有<vi,vj>、<vj,vk>,则必 存在<vj,vk>。显然,在 AOV-网中不能存在回路,否则回路中的活动就会互为前驱,从而无法执行。
- AOV-网的拓扑序列不是惟一的。
例如,上图的另一个拓扑序列为:
那么,怎样求一个有向无环图的拓扑序列呢?拓扑排序(Topological Sort) 的基本思想为:
(1) 从有向图中选一个无前驱的结点输出;
(2) 将此结点和以它为起点的边删除;
(3) 重复(1)、( 2),直到不存在无前驱的结点;
(4) 若此时输出的结点数小于有向图中的顶点数,则说明有向图中存在 回路,否则输出的顶点的顺序即为一个拓扑序列。
例如,对于下图中的 AOV-网,执行上述过程可以得到如下拓扑序列: V1,V6,V4,V3,V2,V5 或 V1,V3,V2,V6,V4,V5。
由于有向图的存储形式的不同,拓扑排序算法的实现也不同。
(1)基于邻接矩阵表示的存储结构
A 为有向图 G 的邻接矩阵,则有
- 找 G 中无前驱的结点 ——在 A 中找到值全为 0 的列;
- 删除以 i 为起点的所有弧 ——将矩阵中 i 对应的行全部置为 0。
算法步骤如下:
①取 1 作为第一新序号;
②找一个未新编号的、值全为 0 的列 j,若找到则转③;否则,若所有的 列全部都编过号,排序结束; 若有列未曾被编号,则该图中有回路;
③输出列号对应的顶点 j,把新序号赋给所找到的列;
④将矩阵中 j 对应的行全部置为 0;
⑤新序号加 1,转②;
(2)基于邻接表的存储结构
此时入度为 0 的顶点即没有前驱的顶点,因此可以附设一个存放各顶点入度 的数组 indegree [ ],于是有
- ①找 G 中无前驱的顶点 ——查找 indegree [ i]为零的顶点 i;
- ②删除以 i 为起点的所有弧——对链在顶点 i 后面的所有邻接顶点 k,将对 应的 indegree[k]减 1。
为了避免重复检测入度为零的顶点,可以再设置一个辅助栈,若某一顶点的入度减为 0,则将它入栈。每当输出某一顶点时,便将它从栈中删除。
【算法思想】 - (1) 首先求出各顶点的入度,并将入度为 0 的顶点入栈;
- (2) 只要栈不空,则重复下面处理:
①将栈顶顶点 i 出栈并打印;
②将顶点 i 的每一个邻接点 k 的入度减 1,如果顶点 k 的入度变为 0,则将 顶点 k 入栈。
【算法描述】 拓扑排序算法
int TopoSort (AdjList G)
{
Stack S;
int indegree[MAX_VERTEX_NUM];
int i, count, k;
ArcNode *p;
FindID(G,indegree); /*求各顶点入度*/
InitStack(&S); /*初始化辅助栈*/
for(i=0;i<G.vexnum;i++)
if(indegree[i]==0) Push(&S,i); /*将入度为 0 的顶点入栈*/
count=0;
while(!IsEmpty(S))
{
Pop(&S,&i);
printf("%c", G.vertex[i].data);
count++; /*输出 i 号顶点并计数*/
p=G.vertex[i].firstarc;
while(p!=NULL)
{
k=p->adjvex;
indegree[k]--; /*i 号顶点的每个邻接点的入度减 1*/
if(indegree[k]==0)
Push(&S, k); /*若入度减为 0,则入栈*/
p=p->nextarc;
}
} /*while*/
if (count<G.vexnum)
return(Error); /*该有向图含有回路*/
else
return(Ok);
}
求入度算法
void FindID( AdjList G, int indegree[MAX_VERTEX_NUM]) /*求各顶点的入度*/
{
int i;
ArcNode *p;
for(i=0; i<G.vexnum; i++)
indegree[i]=0;
for(i=0; i<G.vexnum; i++)
{
p=G.vertex[i].firstarc;
while(p!=NULL)
{
indegree[p->adjvex]++;
p=p->nextarc;
}
} /* for */
}
例如,上图 AOV-网的邻接表如下 所示,用拓扑排序算法求出的拓扑序列为: v6,v1,v3,v2,v4,v5。
若有向无环图有 n 个顶点和 e 条弧,则在拓扑排序的算法中,for 循环需要 执行 n 次,时间复杂度为 O(n);对于 while 循环,由于每一顶点必定进一次栈, 出一次栈,其时间复杂度为 O(e);故该算法的时间复杂度为 O(n+e)。