6.8 拓扑排序
应用:
许多应用问题,都可转化和描述为这一标准形式:给定描述某一实际应用(图(a))的有向图(图(b)),如何在与该图“相容”的前提下,将所有顶点排成一个线性序列(图(c))。
此处的“相容”,准确的含义是:每一顶点都不会通过边,指向其在此序列中的前驱顶点。这样的一个线性序列,称作原有向图的一个拓扑排序(topological sorting)。
有向无环图:
有向无环图的拓扑排序必然存在;反之亦然。这是因为,有向无环图对应于偏序关系,而拓扑排序则对应于全序关系。在顶点数目有限时,与任一偏序相容的全序必然存在。
任一有限偏序集,必有极值元素(尽管未必唯一);类似地,任一有向无环图,也必包含入度为零的顶点。否则,每个顶点都至少有一条入边,意味着要么顶点有无穷个,要么包含环路。
于是,只要将入度为0的顶点m(及其关联边)从图G中取出,则剩余的G'依然是有向无环图,故其拓扑排序也必然存在。从递归的角度看,一旦得到了G'的拓扑排序,只需将m作为最大顶点插入,即可得到G的拓扑排序。如此,我们已经得到了一个拓扑排序的算法:
以下,将转而从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 }
实例:
留意观察各项点的入栈次序,并与6.11对比。
另外,对照图6.11中的结果可见,因多个极大、极小元素(入度、出度为零顶点)并存而导致拓扑排序的不唯一性并未消除,而是转由该算法对每趟DFS起点的选择策略决定。
复杂度:
空间:这里仅额外引入的栈,规模不超过顶点总数O(n)。总体而言,空间复杂度与基本的深度优先搜索算法同样,仍为O(n + e)。
时间:该算法的递归跟踪过程与标准DFS搜索完全一致,且各递归实例自身的执行时间依然保持为O(1),故总体运行时间仍为O(n + e)
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情”