Topological Sort
程度★ 难度★★
楔子
在枚举所有排列的问题之中,如果我们另外再限制谁要排在谁前方、谁要排在谁后方,那么在这些限制之下,合理的排列还会剩下哪些呢?
【注:枚举所有排列,读者们可另行参考「Enumerate all n-tuples」一文。】
先后限制与图
谁要排在谁前方、谁要排在谁后方,其实就是两两之间的关系,故可以改用图来表示:把图上一条由A 点连向B 点的边,想成是A 必须排在B 前方( B 必须排在A 后方)。
当然啦,也可以把图上一条由A 点连向B 点的边,想成是A 必须排在B 后方。不过一般来说我们习惯成自然地使用前者。
Topological Sort 与Topological Order
「拓朴排序」是排序一张有向图的点的方式。把图上一条由A 点连向B 点的边,想成是A 必须排在B 前方( B 必须排在A 后方)。Topological Sort 用来找出合理的排列顺序,让每一个点的先后顺序,满足每一条边所规定的先后顺序。
「拓朴顺序」是指一张有向图经过「拓朴排序」后,每一个点的先后顺序。
一张图经过Topological Sort 的结果可以有很多种。只要不违背图上每一条边的先后规定,要怎么排列图上的点都行。
图上不能有环
当图上有环时,便无法进行Topological Sort 。因为环上每一个点都会有连向自己的边,意味着环上每一个点必须排在其他点的后方,环上每一个点都不能在排列顺序中拔得头筹,所以合理的排列顺序不存在。
Topological Sort:
很普通的演算法
程度★ 难度★
观察问题
要找出合理的排列顺序,首先得决定第一点!知道如何找出第一点,那么就可以循序渐进的再找出第二点、第三点了。
可以作为第一点的点,想必它不必排在其他点后方。也就是说,没有被任何边连向的点,就可以作为第一点。如果有很多个第一点,那么找哪一点都行。
决定第一点之后,那么剩下所有点都会在第一点后方。也就是说,由第一点连出去的边,其先后规定已经被满足了,规定存不存在都无所谓。因此,决定第一点之后,就可以删去此点,以及删去由此点连出去的边──原问题可以递回地缩小!
只要反覆的寻找没有被任何边连向的点,然后删去此点以及删去由此点连出去的边,就可以找出一个合理的排列顺序了。
附带一提,要找出合理的排列顺序,也可以由最后一点开始决定!无论要从第一点找到最后一点,或是从最后一点找到第一点,都是可以的。各位可以想想看该怎么做。
找出一个合理的排列顺序( adjacency matrix )
尽管这个问题有Recursive 的性质,可以用递回实作,但由于递回的分支只有一条,故亦可以用回圈实做。我想大家都会选择以比较简单的回圈方式来实做吧?
实作时可以利用变数纪录图上每一个点目前仍被多少条边连到。寻找没有被任何边连向的点,就直接看该变数是不是零;删去由此点连出去的边,就顺便更新变数的值。
- bool adj [ 9 ][ 9 ]; // adjacency matrix
- int ref [ 9 ]; //纪录图上每一个点目前仍被多少条边连到
- void topological_sort ()
- {
- for ( int i = 0 ; i < 9 ; ++ i ) ref [ i ] = 0 ; //初始化为0
- // 累计图上每一个点被几条边连到
- for ( int i = 0 ; i < 9 ; ++ i )
- for ( int j = 0 ; j < 9 ; ++ j )
- if ( adj [ i ][ j ])
- ref [ j ]++;
- // 开始找出一个合理的排列顺序
- for ( int i = 0 ; i < 9 ; ++ i )
- {
- // 寻找没有被任何边连向的点
- int s = 0 ;
- while ( s < 9 && ref [ s ] != 0 ) ++ s;
- if ( s == 9 ) break ; //找不到。表示目前残存的图是个环。
- ref [ s ] = - 1 ; //设为已找过(删去s点)
- cout << s ; //印出合理的排列顺序的第i点
- // 更新ref的值(删去由s点连出去的边)
- for ( int t = 0 ; t < 9 ; ++ t )
- if ( adj [ s ][ t ])
- ref [ t ]--;
- }
- }
找出一个合理的排列顺序( adjacency lists )
- int adj [ 9 ][ 9 ], size [ 9 ]; // adjacency lists
- int ref [ 9 ]; //纪录图上每一个点目前仍被多少条边连到
- void topological_sort ()
- {
- for ( int i = 0 ; i < 9 ; ++ i ) ref [ i ] = 0 ; //初始化为0
- // 累计图上每一个点被几条边连到
- for ( int i = 0 ; i < 9 ; ++ i )
- for ( int j = 0 ; j < size [ i ]; ++ j )
- ref [ adj [ i ][ j ]]++;
- // 宣告一个queue来纪录已经没有被任何边连向的点
- queue < int > Q ;
- for ( int i = 0 ; i < 9 ; ++ i )
- if ( ref [ i ] == 0 )
- Q . push ( i );
- // 开始找出一个合理的排列顺序
- for ( int i = 0 ; i < 9 ; ++ i )
- {
- // 寻找没有被任何边连向的点
- if ( Q . empty ()) break ; //找不到。表示目前残存的图是个环。
- int s = Q . front (); Q . pop ();
- ref [ s ] = - 1 ; //设为已找过(删去s点)
- cout << s ; //印出合理的排列顺序的第i点
- // 更新ref的值(删去由s点连出去的边)
- for ( int j = 0 ; j < size [ s ]; ++ j )
- {
- int t = adj [ s ][ j ];
- ref [ t ]--;
- if (! ref [ t ]) Q . push ( t ); //纪录已经没有被任何边连向的点
- }
- }
- }
时间复杂度
时间复杂度等于一次Graph Traversal 的时间。图的资料结构为adjacency matrix 的话,便是O(V^2) ;图的资料结构为adjacency lists 的话,便是O(V+E) 。
找出所有合理的排列顺序
请用backtracking 。此处不详述了,直接看练习题吧。
UVa 124
计算所有合理的排列顺序个数
需要使用Dynamic Programming 解决,时间复杂度O(2^V * V^2) 。
Topological Sort:
Depth-first Search
程度★ 难度★★
Depth-first Search 与Topological Sort 的关系
DFS 离开点的顺序,颠倒之后,正好是拓朴顺序。
DFS 优先走到最深的点,直到不能再深为止。DFS 也会优先找出所有最深的点,离开点的原则是最深的点先离开。最深的点当然就是拓朴顺序最后的点。
找出一个合理的排列顺序( adjacency matrix )
- bool adj [ 9 ][ 9 ]; // adjacency matrix
- int visit [ 9 ]; //记录DFS遍历过的点
- int order [ 9 ], n ; //储存一个合理的排列顺序
- bool cycle ; //记录DFS的过程中是否侦测到环
- void DFS ( int s )
- {
- // back edge,有环。
- if ( visit [ s ] == 1 ) cycle = true ;
- // forward edge、cross edge。
- if ( visit [ s ] == 2 ) return ;
- visit [ s ] = 1 ;
- for ( int t = 0 ; t < 9 ; ++ t )
- if ( adj [ s ][ t ])
- DFS ( t );
- visit [ s ] = 2 ;
- order [ n --] = s ; //记录合理的排列顺序
- }
- void topological_sort ()
- {
- // 初始化
- for ( int i = 0 ; i < 9 ; i ++) visit [ i ] = 0 ;
- cycle = false ;
- n = 9 - 1 ;
- // 进行DFS
- for ( int s = 0 ; s < 9 ; ++ s )
- if (! v [ s ])
- DFS ( s );
- // 输出结果
- if ( cycle )
- cout << "图上有环" ;
- else
- // 印出一个合理的排列顺序
- for ( int i = 0 ; i < 9 ; ++ i )
- cout << order [ i ];
- }