hdu 拓扑排序 题目归纳

                                                       拓扑排序


定义和前置条件:

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


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

 

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


偏序/全序关系:

偏序和全序实际上是离散数学中的概念。

这里不打算说太多形式化的定义,形式化的定义教科书上或者上面给的链接中就说的很详细。

 

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

 

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

 

实际上,很多地方都存在偏序和全序的概念。

比如对若干互不相等的整数进行排序,最后总是能够得到唯一的排序结果(从小到大,下同)。这个结论应该不会有人表示疑问吧:)但是如果我们以偏序/全序的角度来考虑一下这个再自然不过的问题,可能就会有别的体会了。

 

那么如何用偏序/全序来解释排序结果的唯一性呢?

我们知道不同整数之间的大小关系是确定的,即1总是小于4的,不会有人说1大于或者等于4吧。这就是说,这个序列是满足全序关系的。而对于拥有全序关系的结构(如拥有不同整数的数组),在其线性化(排序)之后的结果必然是唯一的。对于排序的算法,我们评价指标之一是看该排序算法是否稳定,即值相同的元素的排序结果是否和出现的顺序一致。比如,我们说快速排序是不稳定的,这是因为最后的快排结果中相同元素的出现顺序和排序前不一致了。如果用偏序的概念可以这样解释这一现象:相同值的元素之间的关系是无法确定的。因此它们在最终的结果中的出现顺序可以是任意的。而对于诸如插入排序这种稳定性排序,它们对于值相同的元素,还有一个潜在的比较方式,即比较它们的出现顺序,出现靠前的元素大于出现后出现的元素。因此通过这一潜在的比较,将偏序关系转换为了全序关系,从而保证了结果的唯一性。

拓展到拓扑排序中,结果具有唯一性的条件也是其所有顶点之间都具有全序关系。如果没有这一层全序关系,那么拓扑排序的结果也就不是唯一的了。在后面会谈到,如果拓扑排序的结果唯一,那么该拓扑排序的结果同时也代表了一条哈密顿路径。

拓扑排序的三种方法:

1.无前趋的的顶点优先拓扑排序

    思路:在有向图建立完成之后,维护两个点集,一个是当前出度为0的点集,记为①,另一个是出度不为0 的点集,记为②,以及一个记录各个点出度的数组。首先遍历一遍图的全部边,初始化所有点的出度,然后出度为0的点依次 入①,然后将①中的点分别出列,每次出列都需要更新各个点的出度,即把所有跟出列的点邻接的点出度-1(有多条边,则相应减掉边数,一般简单图不会有多重边),直至①变成空集。这个时候,如果②也变成了空集,证明排序成功,否则,原图不存在拓扑排序(图中有环)。最终的排序结果就是从①中出列的点的逆序


2.无后继的的顶点优先拓扑排序

  思路:跟1的方法类似,不过这次是维护根据点的入度进行统计。在有向图建立完成之后,维护两个点集,一个是当前入度为0的点集,记为①,另一个是入度不为0 的点集,记为②,以及一个记录各个点入度的数组。首先遍历一遍图的全部边,初始化所有点的入度,然后入度为0的点依次 入①,然后将①中的点分别出列,每次出列都需要更新各个点的入度,即把所有跟出列的点邻接的点入度-1(有多条边,则相应减掉边数,一般简单图不会有多重边),直至①变成空集。这个时候,如果②也变成了空集,证明排序成功,否则,原图不存在拓扑排序(图中有环)。最终的排序结果就是从①中出列的点的顺序


3.基于DFS递归的拓扑排序

  思路:从图的起点开始进行深度优先搜索,在搜索过程中,把没有后继(相当于出度为0)的点出列(这个过程中,已经出列的点不算是它的前继点,相当于删除了该点),点的出列顺序就是拓扑排序结果的逆序


下面分析一些hdu上的题目来考察这三个方法的异同。

1.前两种方法本质上是一样的,只不过一个得到的是顺序,一个是逆序,这就根据情况和喜好进行判断,对于关系(a,b)我们直观上认为在图中是这样的 a -> b, 然而,在某些题目中(a,b)的意义可能是 a>b,这就不大符合我们的直观理解(一般认为图的上端好,大……),不过这都不影响排序结果,各求所需就好。

2.未经优化的DFS拓扑排序,在图存在环的时候会进入死循环,因此,要注意确保图没有环,或者最好进行优化再使用。

3.维护出度为0以及DFS拓扑得到的结果是逆序!

4.拓扑排序结果不一定唯一,注意题目要求。

5.DFS拓扑需要知道图的起点,否则不能深搜整个图,也就没有得到完整的拓扑排序结果。

6.在维护点集的拓扑中,加入当前出度(入度)为0的点大于1个,则得到的拓扑排序结果不唯一


hdu1285:点击打开链接

比较原汁原味的拓扑排序,要注意“小号”优先,

#include <cstdio>
#include <iostream>
#include <list>
#include <cstring>
using namespace std;

class Graph
{
public:
    int v;
    list<int> *adj;
    list<int> result;

    Graph(int vertice)
    {
        v = vertice;
        adj = new list<int>[v];
    }

    void add_edge(int st,int ed)
    {
        adj[st].push_back(ed);
    }

    void topological_sort()
    {
        list<int> zeroNode;
        int degree[v];
        list<int>:: iterator j,it;
        memset(degree,0,sizeof(degree));
        for(int i = 0; i < v; i++)
        {
            for(j = adj[i].begin(); j != adj[i].end(); j++)
                degree[*j]++;
        }
        for(int i = 0; i < v; i++)
            if(degree[i] == 0)
                zeroNode.push_back(i);
        while(zeroNode.size() > 0)
        {
            int top = v;
            for(j = zeroNode.begin(); j != zeroNode.end(); j++)
            {
                if(*j < top)
                {
                    top = *j;
                    it = j;
                }
            }
            result.push_back(top);
            zeroNode.erase(it);
            for(j = adj[top].begin(); j != adj[top].end(); j++)
            {
                degree[*j]--;
                if(degree[*j] == 0)
                    zeroNode.push_back(*j);
            }
        }
    }
};


int main()
{
    int v,e;
    while(cin >> v >> e)
    {
        int st,ed;
        Graph g(v);
        for(int i = 0; i < e; i++)
        {
            scanf("%d%d",&st,&ed);
            g.add_edge(st-1,ed-1);
        }
        g.topological_sort();
        list<int>:: iterator it;
        while(g.result.size() > 1)
        {
            printf("%d ",g.result.front()+1);
            g.result.pop_front();
        }
        printf("%d\n",g.result.front()+1);
    }

    return 0;
}

hdu3342:点击打开链接

这里不需要排序结果,只要在排序过程中判断是否存在环即可,这种情况不建议使用DFS递归排序,因为未经优化的DFS排序,只能对无环DAG排序,要判断是否有环,需要另外处理,而另外两种方法,本身就可以判断,拓扑排序结果是否存在(即图是否有环)。

#include <iostream>
#include <cstdio>
#include <list>
#include <cstring>
#include <cstdlib>
using namespace std;

class Graph
{
public:
    int v;//顶点数量
    list<int> *adj; //邻接表

    Graph(int v)
    {
        this -> v = v;
        adj = new list<int>[v];
    }

    void add_Edge(int st,int ed)
    {
        adj[st].push_back(ed);
    }

    void topological_sort()
    {
        list<int>::iterator j;
        int degree[v]; //统计各个点的入度
        memset(degree,0,sizeof(degree));
        for(int i = 0;i < v; i++)
        {
            for(j = adj[i].begin(); j != adj[i].end(); j++)
            {
                degree[*j]++;
            }
        }

        
        list<int> zero_node;//当前入度为0的点
        list<int> result;//已经排序的点集
       
        for(int i = 0; i < v; i++)
        {
            if(degree[i] == 0)
                zero_node.push_back(i);
        }
        while(zero_node.size() > 0)
        {
            int top = zero_node.back();
            zero_node.pop_back();
            result.push_back(top);
            for(j = adj[top].begin(); j != adj[top].end();j++)
            {
                degree[*j]--;
                if(degree[*j] == 0)
                    zero_node.push_back(*j);
            }
        }

        if(result.size() == v)
            cout << "YES\n";
        else
            cout << "NO\n";

    }
};



int main()
{
    int v,e;
    while(cin >> v >> e)
    {
        if(v == 0)
            break;
        Graph g(v);
        int st,ed;
        for(int i = 0; i < e; i++)
        {  
            scanf("%d%d",&st,&ed);
            g.add_Edge(st,ed);
        }
        g.topological_sort();
    }

    return 0;
}

 hdu2647:

点击打开链接

这道题是比较典型的拓扑排序,不过区别就是这道题不仅需要拓扑排序,而且要分出层次,因为题目要求money最少,所以应该从第一层的888开始,每一层都只比上一层增加1元,所以,这里使用的维护入度为0的排序,跟一般的有一点小区别,那就是当前入度为0的点集要统一出列,这样就能做到同一层次的点统一定价。

#include <cstdio>
#include <iostream>
#include <list>
#include <cstdlib>
#include <cstring>
using namespace std;

class Graph
{
public:
    int v;
    list<int> *adj;
    int *reward;

    Graph(int vertice)
    {
        v = vertice;
        adj = new list<int>[v];
        reward = new int[v];
        memset(reward,0,sizeof(int)*v);
    }

    void add_edge(int st,int ed)
    {
        adj[st].push_back(ed);
    }

    void topological_sort()
    {
        int degree[v];
        memset(degree,0,sizeof(degree));
        list<int>::iterator j,h;
        for(int i = 0; i < v; i++)
        {
            for(j = adj[i].begin(); j != adj[i].end();j++)
                degree[*j]++;
        }
        list<int> zeroNode;
        list<int> result;
        for(int i = 0; i < v; i++)
            if(degree[i] == 0)
                zeroNode.push_back(i);
        int cut_money = 888;
        while(zeroNode.size() > 0)
        {
            for(j = zeroNode.begin(); j != zeroNode.end();j++)
            {
                reward[*j] = cut_money;
                result.push_back(*j);
                int top = *j;
                for(h = adj[top].begin(); h != adj[top].end(); h++)
                {
                    degree[*h]--;
                }
            }
            zeroNode.clear();
            cut_money++;
            for(int i = 0; i < v; i++)
            {
                if(degree[i] == 0 && reward[i] == 0)
                    zeroNode.push_back(i);
            }
        }
        /*
        for(int i = 0; i < v; i++)
            cout << reward[i] << " ";
        cout << endl;
        */
        if(result.size() == v)
        {
            long long total = 0;
            for(int i = 0; i < v; i++)
                total += (long long)reward[i];
            cout << total << endl;
        }
        else
            cout << "-1\n";
    }
};

int main()
{
    int v,e;
    while(cin >> v >> e)
    {
        Graph g(v);
        int st,ed;
        for(int i = 0; i < e; i++)
        {
            scanf("%d%d",&ed,&st);
            g.add_edge(st-1,ed-1);
        }
        g.topological_sort();
    }
    return 0;
}


    原文作者:拓扑排序
    原文地址: https://blog.csdn.net/simon_coder/article/details/51557905
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞