拓扑排序
目录:
拓扑排序
拓扑排序的若干种实现方式
DFS
邻接表+暴搜
BFS+队列
BFS+优先队列
BFS+优先队列+贪心思想
拓扑排序的题目
一.拓扑排序:
什么是拓扑排序?(参考维基百科)
拓扑排序:在图论中,有一个有向为无回路图(DAG)的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序(Topological sorting).
1.每个顶点出现且只出现一次
2.若A在序列中排在B的前面,则图中不存在从B到A的路径
亦可定义:
拓扑排序是对有向无回路图的顶点的一种排序,它使得如果存在一条从顶点A到顶点B的路径,那么在排序中B出现在A的后面。
在实际的应用:有向无回路图描述的是事件发生的先后顺序,可以这样定义,如果事件A在事件B前面发生:那么可以连一条从A到B的有向边。那么在进行玩拓扑排序后,A一定在B的前面。
举个例子:某个人早上起来穿衣服的过程
他必须先穿好某些衣服,才能在穿其他的衣服(如先穿袜子,在穿鞋),其他的衣服则可以按任意次序穿戴(如袜子与裤子)。
二.拓扑排序的各种实现方式:
1.DFS版(参考自《算法导论》)
算法步骤:
Topological-Sort(G)
1.call DFS to coumpute finishing times F[v] to each vertex v.
2.As each is finished ,insert it onto the front of a linked list
3.Return the linked list of vertices
这种实现方式,换句话说按如下方式:
首先,对输入的有向无环图G进行DFS,在进行DFS的时候记录一个时间戳,在对每一个点进行访问的时候我们将 其对应的时间给记录下来,当该点已经访问过,返回上一个节点的时候,我们将对应的从该点退出的事件也记录下 来,然后在按照对应的退出时间进行排序。
DFS方式是一种常用的求拓扑序列的方式。
2.邻接表+暴搜(邻接矩阵也可以)
将图采用邻接表的方式进行存储,统计每个点的入度。每次将入度为0的点加入到结果数组的前面。算法步骤:
1.Init mark[] , degree[] ; mark[] 用于标记那些点已经被访问过,degree[]用于记录每个点的入度。
2.For i : 1-->n ; n为点的个数
3. For j : 1-> n
4. If(!mark[j] && degree[j]=0)
5. Break ;
6. mark[j] = 1 ;
7. Add j to result set
8. For edge (j ,k)
9. degree[k] -- ;
10.END
这个算法的主要思想就是每次寻找一个入度为0的点,然后将入度为0的点输出,同时将以该点为起点的边全部删除,对应的也就是以该点为起点的边的终点入度减一。这种方式也是实现的一种常用方式。但是复杂度比较高O(n^2)。由于每一次总是在查询入度为0的点复杂度为O(n),所以每次都要遍历,这导致了效率比较低。一种可能的优化方式:利用队列将入度为0的节点存下来,这样每次获取入度为0的点,就只需要O(1)的复杂度.这样就比较快了。
3.BFS+队列
这种方式的实现思想也和上一种差不多也是每次寻找入度为0的点,然后将与其相关的边删除,将新的入度为0的点入队列。算法步骤如下:
1. 把所有入度为0的节点放进队列Q中
2. While : !Q.empty()
3. A = Q.front()
4. Q.pop()
5. Add A to result set
6. For : each edge (A , x)
7. degree[x] --
8. If degree[x] = 0
9. Q.push(A)
10. END
这种方式与实现队列方式一样,不过每次是扩展那些入度为0的点进入队列。这样实现的拓扑排序的效率就高了不少,每个节点进入队列一次,然后每个节点的边被访问一次,故而效率为O(V+E),V为图的顶点数,E为图的边数。这就比前面的一般实现方式快得多。特别是当点数比较多的时候,效率的差别就会体现的很明显。
4.BFS+优先队列
某些情况下,我们可能会去求拓扑排序的字典序优先的方式,那么如何求解字典序优先的方式呢?在寻找入度为0的点的时候,我们可以每次选择编号最小的点,做为当前的扩展的目标。这样我们就可以得到字典序优先的方式。当然我们可以用第二种方式,在搜索入度为0的点的时候,我们每次选择编号比较小的方式。当然我们还是可以采用第三种方式来进行求解的,不过这里的队列就不再是普通的队列,我们得采用优先队列来实现。在取出最小编号的节点的时候,我们直接采用优先队列来获取编号最小的节点。算法步骤如下:
1.将入度为0的所有的点入优先队列PQ。
2.While: !PQ.empty()
3. A = PQ.top()
4. PQ.pop()
5. Add A to the result set
6. For each edge (A , x)
7. degree[x]--
8. If degree[x] = 0
9. Then add x to PQ
10.END
然后我们就可以很快的求出字典序优先的拓扑排序了。
在前面的拓扑排序的实现方式都是以入度为0来进行排序的,当然我们也可以以出度为0的方式进行拓扑排序。
5.BFS+优先队列+贪心思想
在POJ3687那个题目中要求的是求解编号最小的点放在前面。采用前面的方式都不能得到对应的解。我们可以这样考虑:编号小的不一定放在前面,但是节点编号大的一定放在后面。如果节点编号大的放在前面,那么肯定无法得到题目要求的解。有了这个思想(贪心),由于要将节点编号大的放在后面,那么就只能从出度为0的点开搜了。那么这个题目就可以解决了。算法步骤如下:
1. 把所有出度为0的点加入到优先队列PQ中
2. While:!PQ.empty()
3. A = PQ.top()
4. PQ.pop()
5. Add A to result set
6. For edge (A , x)
7. degree[x]--
8. IF degree[x] = 0
9. Add x to PQ
10. END
拓扑排序的题目:
在拓扑排序时要求的是:有向无环图。那么对于有回路的怎么判断? 在进行拓扑排序的时候我们很容易发现,如果存在回路,那么必然在某个时刻没有入度为0的点(出度为0的点),由此我们便可以很容易判断出是不是存在环路。下面来看两个比较经典的题目:
POJ1094,这个题目需要不断的调用拓扑排序来计算是不是产生解,或者无解的情况。对于环路的情况我们很容易判断,那么对于结果可能有多种的怎么搞呢?产生多种结果的原因不就是同时有多个入度为0的点么?那这个题就OK了。注意判断条件就行了。
#include<iostream>
#include<string.h>
#include<stdio.h>
#include<vector>
using namespace std ;
#define MAXN 30
vector<int> G[MAXN] ;
int degree[MAXN] ;
bool mark[MAXN] ;
char word[MAXN] ;
int N ;
int R ;
int topsort(){
memset(degree , 0 ,sizeof(degree)) ;
memset(mark , 0 , sizeof(mark)) ;
memset(word , 0 , sizeof(word)) ;
//caculate the degree of every node
for(int i = 0 ; i < N ; i ++){
for(int j = 0 ; j < G[i].size() ; j ++){
degree[ G[i][j] ] ++ ;
}
}
bool flag = 0 ;
for(int i = 0 ; i < N ; i ++){
int p ;
int q ;
p = -1;
q = 0 ;
for(int j = 0 ; j < N ; j ++){
if(p == -1 && !mark[j] && degree[j] == 0 ){
p = j ;
q ++ ;
}
else if(!mark[j] && degree[j] == 0){
q ++ ;
}
}
//not found
if(q == 0)
return 2 ;
//more than one
if(q > 1)
flag = 1 ;
word[i] = p + 'A' ;
mark[p] = 1 ;
for(int k = 0 ; k < G[p].size() ; k ++){
degree[ G[p][k] ] -- ;
}
}
if(flag)
return 1 ;
return 0 ;
}
void init(){
for(int i = 0 ; i < N ; i ++)
G[i].clear() ;
}
void solve(){
init() ;
char tp[5] ;
int i ;
bool flag ;
flag = 0 ;
for(int i = 0 ; i < R ; i ++){
cin>>tp ;
if(flag)
continue ;
int x = tp[0] - 'A' ;
int y = tp[2] - 'A' ;
G[x].push_back(y) ;
int res = topsort() ;
if(res == 2){
cout<<"Inconsistency found after "<<i + 1<<" relations."<<endl;
flag = 1 ;
}
else if(res == 0){
cout<<"Sorted sequence determined after "<<i + 1<<" relations: "<<word<<"."<<endl ;
flag = 1 ;
}
}
if(!flag)
cout<<"Sorted sequence cannot be determined."<<endl ;
}
int main(){
while(cin>>N>>R){
if(N==0 && R==0)
break ;
solve() ;
}
return 0 ;
}
POJ3687,这个题目已经在前面说过了~~!
#include<iostream>
#include<stdio.h>
#include<queue>
#include<string.h>
#include<vector>
using namespace std ;
const int maxn = 205 ;
//用于记录边的信息
bool G[maxn][maxn] ;
bool mark[maxn] ;
//用于记录出度
int degree[maxn] ;
//用于记录结果
int res[maxn] ;
int N ;
int M ;
struct cmp{
bool operator()(int a ,int b){
return a < b ;
}
};
int bfs_topsort(){
priority_queue<int , vector<int> , cmp> Q ;
memset(res , 0 , sizeof(res) ) ;
memset(degree ,0 , sizeof(degree) ) ;
memset(mark , 0 , sizeof(mark)) ;
//求解出度表
for(int i = 1 ; i <= N ; i ++){
for(int j = 1 ; j <= N ; j ++){
degree[i] += G[i][j] ;
}
if(degree[i]==0){
Q.push(i) ;
mark[i] = 1 ;
}
}
int k = N ;
while(!Q.empty() ){
int x = Q.top() ;
Q.pop() ;
res[x] = k-- ;
for(int i = 1 ; i <= N ; i ++){
//如果i到x有一条边,那么将i的出度-1
if(G[i][x]){
degree[i] -- ;
}
if(!mark[i] && degree[i]==0){
Q.push(i) ;
mark[i] = 1 ;
}
}
}
//如果出现环路的话,最后的结果K一定不为0
if(k != 0){
return -1 ;
}
return 0 ;
}
int main(){
int T ;
scanf("%d" , &T) ;
while(T--){
scanf("%d%d" , &N , &M) ;
int x ;
int y ;
memset(G , 0 , sizeof(G)) ;
while(M--){
scanf("%d%d", &x , &y) ;
G[x][y] = 1 ;
}
int d = bfs_topsort() ;
if(d == -1){
printf("-1\n") ;
}
else{
for(int k = 1 ; k <= N ; k ++){
if(k != 1){
printf(" ");
}
printf("%d" , res[k]) ;
}
printf("\n") ;
}
}
return 0 ;
}
拓扑排序是对有向无回路图的顶点排序,确保无从后向前的边。本文介绍了拓扑排序的概念,包括DFS、邻接表+暴搜、BFS+队列、BFS+优先队列等不同实现方式,并讨论了优化和字典序优先的策略。拓扑排序在事件发生的先后顺序问题中有着实际应用,例如事件依赖的排序。
3315

被折叠的 条评论
为什么被折叠?



