数据结构 | 第6章 拓扑排序

198 阅读4分钟

6.8 拓扑排序

应用:

许多应用问题,都可转化和描述为这一标准形式:给定描述某一实际应用(图(a))的有向图(图(b)),如何在与该图“相容”的前提下,将所有顶点排成一个线性序列(图(c))。

此处的“相容”,准确的含义是:每一顶点都不会通过边,指向其在此序列中的前驱顶点。这样的一个线性序列,称作原有向图的一个拓扑排序(topological sorting)。

image-20220714152639773.png

有向无环图:

有向无环图的拓扑排序必然存在;反之亦然。这是因为,有向无环图对应于偏序关系,而拓扑排序则对应于全序关系。在顶点数目有限时,与任一偏序相容的全序必然存在。

任一有限偏序集,必有极值元素(尽管未必唯一);类似地,任一有向无环图,也必包含入度为零的顶点。否则,每个顶点都至少有一条入边,意味着要么顶点有无穷个,要么包含环路。

于是,只要将入度为0的顶点m(及其关联边)从图G中取出,则剩余的G'依然是有向无环图,故其拓扑排序也必然存在。从递归的角度看,一旦得到了G'的拓扑排序,只需将m作为最大顶点插入,即可得到G的拓扑排序。如此,我们已经得到了一个拓扑排序的算法:

image-20220714161310575.png

以下,将转而从DFS入手,给出另一拓扑排序算法:

有限偏序集中也必然存在极小元素(同样,未必唯一)。该元素作为顶点,出度必然为零——比如图6.10(b)中的顶点D和F。而在对有向无环图的DFS搜索中,首先因访问完成而转换至VISITED状态的顶点m,也必然具有这一性质;反之亦然。

进一步地,根据DFS搜索的特性,顶点m(及其关联边)对此后的搜索过程将不起任何作用。于是,下一转换至VISITED状态的顶点可等效地理解为是,从图中剔除顶点m(及其关联边)之后的出度为零者在拓扑排序中,该顶点应为顶点m的前驱。由此可见,DFS搜索过程中各顶点被标记为VISITED的次序,恰好(按逆序)给出了原图的一个拓扑排序。

此外,DFS搜索善于检测环路的特性,恰好可以用来判别输入是否为有向无环图。具体地,搜索过程中一旦发现后向边,即可终止算法并报告“因非DAG而无法拓扑排序”。

 0001 template <typename Tv, typename Te> //基于DFS的拓扑排序算法
 0002 Stack<Tv>* Graph<Tv, Te>::tSort ( Rank s ) { //assert: 0 <= s < n
 0003    reset(); int clock = 0; Rank v = s;
 0004    Stack<Tv>* S = new Stack<Tv>; //用栈记录排序顶点
 0005    do {
 0006       if ( UNDISCOVERED == status ( v ) )
 0007          if ( !TSort ( v, clock, S ) ) { //clock并非必需
 0008             while ( !S->empty() ) //任一连通域(亦即整图)非DAG
 0009                S->pop(); break; //则不必继续计算,故直接返回
 0010          }
 0011    } while ( s != ( v = ( ++v % n ) ) );
 0012    return S; //若输入为DAG,则S内各顶点自顶向底排序;否则(不存在拓扑排序),S空
 0013 }
 0014 
 0015 template <typename Tv, typename Te> //基于DFS的拓扑排序算法(单趟)
 0016 bool Graph<Tv, Te>::TSort ( Rank v, int& clock, Stack<Tv>* S ) { //v < n
 0017    dTime ( v ) = ++clock; status ( v ) = DISCOVERED; //发现顶点v
 0018    for ( Rank u = firstNbr ( v ); -1 < u; u = nextNbr ( v, u ) ) //枚举v的所有邻居u
 0019       switch ( status ( u ) ) { //并视u的状态分别处理
 0020          case UNDISCOVERED:
 0021             parent ( u ) = v; type ( v, u ) = TREE;
 0022             if ( !TSort ( u, clock, S ) ) //从顶点u处出发深入搜索
 0023                return false; //若u及其后代不能拓扑排序(则全图亦必如此),故返回并报告
 0024             break;
 0025          case DISCOVERED:
 0026             type ( v, u ) = BACKWARD; //一旦发现后向边(非DAG),则
 0027             return false; //不必深入,故返回并报告
 0028          default: //VISITED (digraphs only)
 0029             type ( v, u ) = ( dTime ( v ) < dTime ( u ) ) ? FORWARD : CROSS;
 0030             break;
 0031       }
 0032    status ( v ) = VISITED; S->push ( vertex ( v ) ); //顶点被标记为VISITED时,随即入栈
 0033    return true; //v及其后代可以拓扑排序
 0034 }

实例:

image-20220714163754045.png

留意观察各项点的入栈次序,并与6.11对比。

另外,对照图6.11中的结果可见,因多个极大、极小元素(入度、出度为零顶点)并存而导致拓扑排序的不唯一性并未消除,而是转由该算法对每趟DFS起点的选择策略决定。

复杂度:

空间:这里仅额外引入的栈,规模不超过顶点总数O(n)。总体而言,空间复杂度与基本的深度优先搜索算法同样,仍为O(n + e)。

时间:该算法的递归跟踪过程与标准DFS搜索完全一致,且各递归实例自身的执行时间依然保持为O(1),故总体运行时间仍为O(n + e)

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情