有向图的拓扑排序

来源:互联网 发布:尚学堂 js 编辑:程序博客网 时间:2024/05/16 06:42

1. 拓扑排序的概念

定义:将有向图中的顶点以线性方式进行排序。即对于任何连接自顶点u到顶点v的有向边u->v,在最后的排序结果中,顶点u总是在顶点v的前面。

考虑一个非常非常经典的例子——选课。假设我非常想学习一门机器学习的课程,但是在修这么课程之前,我们必须要先学习一些基础课程,比如计算机科学概论,C语言程序设计,数据结构,算法等等。那么这个制定选修课程顺序的过程,实际上就是一个拓扑排序的过程。每门课程相当于有向图中的一个顶点,而连接顶点之间的有向边就是课程学习的先后关系。只不过这个过程不是那么复杂,从而很自然的在我们的大脑中完成了。将这个过程以算法的形式描述出来的结果,就是拓扑排序。

2. 拓扑排序的充要条件

是不是所有的有向图都能被拓扑排序呢?显然不是。
继续考虑上面的例子,如果告诉你在选修《计算机科学概论》这门课之前需要你先学习《机器学习》,你是不是会被弄糊涂?在这种情况下,就无法进行拓扑排序,因为它中间存在互相依赖的关系,从而无法确定谁先谁后。在有向图中,这种情况被描述为存在环路
因此,一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。

3. 偏序和全序的关系

还是以上面选课的例子来描述这两个概念。假设我们在学习完了《算法》这门课后,可以选修《机器学习》或者《计算机图形学》。这也就意味着,学习《机器学习》和《计算机图形学》这两门课之间没有特定的先后顺序。因此,在我们所有可以选择的课程中,任意两门课程之间的关系要么是确定的(即拥有先后关系),要么是不确定的(即没有先后关系),绝对不存在互相矛盾的关系(即环路)。
以上就是偏序的意义,抽象而言,有向图中两个顶点之间不存在环路,至于连通与否,是无所谓的。所以,有向无环图必然是满足偏序关系的。


理解了偏序的概念,那么全序就好办了。所谓全序,就是在偏序的基础上,有向无环图中的任意一对顶点还需要有明确的关系(反映在图中,就是单向连通的关系,注意不能双向连通,那就成环了)。可见,全序就是偏序的一种特殊情况
回到我们的选课例子中,如果《机器学习》需要在学习了《计算机图形学》之后才能学习,那么它们之间也就存在了确定的先后顺序,原本的偏序关系就变成了全序关系。


实际上,很多地方都存在偏序和全序的概念。
比如对若干互不相等的整数进行排序,最后总是能够得到唯一的排序结果。我们以偏序/全序的角度来考虑一下这个再自然不过的问题,可能就会有别的体会了。

如何用偏序/全序来解释排序结果的唯一性呢?
我们知道不同整数之间的大小关系是确定的,比如1总是小于4的。这就是说,这个序列是满足全序关系的。而对于拥有全序关系的结构(如拥有不同整数的数组),在其线性化(排序)之后的结果必然是唯一的。
对于排序的算法,我们评价指标之一是看该排序算法是否稳定,即值相同的元素的排序结果是否和出现的顺序一致。比如,我们说快速排序是不稳定的,因为最后的快排结果中,相同元素的出现顺序和排序前不一致了。如果用偏序的概念可以这样解释这一现象:相同值的元素之间的关系是无法确定的。因此它们在最终的结果中的出现顺序可以是任意的。而对于诸如插入排序这种稳定性排序,它们对于值相同的元素,还有一个潜在的比较方式,即比较它们的出现顺序,出现靠前的元素大于出现后出现的元素。因此通过这一潜在的比较,将偏序关系转换为了全序关系,从而保证了结果的唯一性。
拓展到拓扑排序中,结果具有唯一性的条件也是其所有顶点之间都具有全序关系。如果没有这一层全序关系,那么拓扑排序的结果也就不是唯一的了。如果拓扑排序的结果唯一,那么该拓扑排序的结果同时也代表了一条哈密顿路径。

4. 入度和出度

入度和出度是图论算法中重要的概念之一。
入度(in-degree)通常指某点作为有向边终点的次数,即有多少条边指向该点。
出度 (out-degree) 是指从该点出发的有向边的数目。

5. 拓扑排序的实现

拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止。

  • 选择一个入度为0的顶点并输出之;
  • 从网中删除此顶点及所有出边(以它为起点的有向边)。

直至图空,或者图不空但找不到无前驱的顶点为止。
循环结束后,若输出的顶点数小于图中的总顶点数,说明图中存在回路;否则图中不存在回路,输出的顶点序列就是一种拓扑序列。

我们继续以题来进行进一步讲解:

Description
有N个比赛队(1<=N<=500),编号依次为1,2,3,。。。。,N进行比赛,比赛结束后,裁判委员会要将所有参赛队伍从前往后依次排名,但现在裁判委员会不能直接获得每个队的比赛成绩,只知道每场比赛的结果,即P1赢P2,用P1,P2表示,排名时P1在P2之前。现在请你编程序确定排名。

输入

第一行是2个数N(1<=N<=500)和M;其中N表示队伍的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2,表示即P1队赢了P2队。

输出

给出一个符合要求的排名。输出时队伍号之间有空格,最后一名后面没有空格。
其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。

Sample Input4 3 1 2 2 3 4 3Sample Output1 2 4 3

完整代码实现:

采用邻接矩阵实现,map[i][j]=0,表示节点i和j没有关联;map[i][j]=1,表示存在边<i,j>,并且是从i指向j。

#include <iostream>#include <vector>#include <string>#include<stack>#include<queue>#include<set>#include<map>#include <numeric>#include<limits.h>#include <algorithm>using namespace std;int main() {    int t;    cin>>t;    while(t--) {        int n,m;//课程总数和顺序关系的数量        cin>>n>>m;        int i,j;        vector<int> in(n+1,0);//记录每个节点的入度        vector<int> book(n+1,0);//记录该节点是否已经被确定        vector<vector<int>> map(n+1,vector<int>(n+1,0));//记录两个节点的顺序关系        int from,to;        for(i=1;i<=m;i++) {//初始化每个节点的入度            cin>>from>>to;            map[from][to]=1;//表示存在节点from到结点to的顺序关系            in[to]++;        }        while(1) {            int index=0;//当前这轮确定了的节点编号            bool valid = 1;            for(i=1;i<=n;i++)                if(book[i]==0) {                    valid = 0;                    break;                }            if(valid) {//如果所有节点都已经访问,说明该组数据是合法的,直接输出                cout<<"Correct"<<endl;                break;            }            bool find = false;            for(i=1;i<=n;i++) {                if(book[i]==0 && in[i]==0) {//若某个未确定节点的入度为0                    index=i;                    book[i]=1;                    find = true;                    break;                }            }            if(!find) {//如果在未确定的节点中,找不到入度为0的                cout<<"Wrong"<<endl;                break;            }            for(i=1;i<=n;i++)                if(book[i]==0 && map[index][i]==1) //若某个节点未被确定,而且节点index是它的前置节点,就将其入度减一                    in[i]--;        }    }    return 0;}

如果采用邻接表存储边的信息,那么可以写成:

int in[100001];//记录每个节点的入度vector<int> edge[100001]; //邻接表int main() {    int t;    cin>>t;    while(t--) {        int n,m;//课程总数和顺序关系的数量        int from,to;        int i,k;        int index;        int cnt=0;        cin>>n>>m;        for(i=1;i<=n;i++) {//初始化            in[i]=0;            edge[i].clear();        }        for(i=1;i<=m;i++) {//初始化每个节点的入度            cin>>from>>to;            edge[from].push_back(to);            in[to]++;        }        for(k=1;k<=n;k++) {            index=0;//当前这轮确定了的节点编号            bool valid = false;            for(i=1;i<=n;i++) {                if(in[i]==0) {//若某个未确定节点的入度为0                    index=i;                    cnt++;                    in[i]--;                    valid = true;                    break;                }            }            if(!valid) {//如果在未确定的节点中,找不到入度为0的                cout<<"Wrong"<<endl;                break;            }            for(auto p : edge[index]) {//删除从节点index出发的边,将终点的入度减一                in[p]--;            }        }        if(cnt==n)  cout<<"Correct"<<endl;    }    return 0;}
0 0