拓扑排序

拓扑排序是对有向无回路图的顶点排序,确保无从后向前的边。本文介绍了拓扑排序的概念,包括DFS、邻接表+暴搜、BFS+队列、BFS+优先队列等不同实现方式,并讨论了优化和字典序优先的策略。拓扑排序在事件发生的先后顺序问题中有着实际应用,例如事件依赖的排序。

拓扑排序

目录:

  拓扑排序

  拓扑排序的若干种实现方式

    DFS

    邻接表+暴搜

    BFS+队列

    BFS+优先队列

    BFS+优先队列+贪心思想

    拓扑排序的题目

一.拓扑排序:

 什么是拓扑排序?(参考维基百科)

 拓扑排序:在图论中,有一个有向为无回路图(DAG)的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序(Topological sorting).

  1.每个顶点出现且只出现一次

  2.若A在序列中排在B的前面,则图中不存在从BA的路径

  亦可定义:

  拓扑排序是对有向无回路图的顶点的一种排序,它使得如果存在一条从顶点A到顶点B的路径,那么在排序中B出现在A的后面。

  在实际的应用:有向无回路图描述的是事件发生的先后顺序,可以这样定义,如果事件A在事件B前面发生:那么可以连一条从AB的有向边。那么在进行玩拓扑排序后,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 ;
}



 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值