图的拓扑排序详解与实现——深搜(递归、非递归)、广搜

基本概念

有向无环图(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 TOPOLOGICALSORT 生成的是有向无环图的拓扑排序

假定在有向无环图 G = ( V , E ) G=(V,E) G=(V,E) 上运行 DFS 来计算节点的完成时间(结束时间)。我们只需要证明,对于任意一对不同的节点 u , v ∈ V u,v\in V u,vV,如果图 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);
    }
}
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值