算法专题:Topological Sort

拓扑排序(Topological Sort)是针对有向无环图(DAG)的一种排序方式,使得在图中uv路径为从u到v的排序结果中,u始终出现在v前面。
比如说,学功课C需要先学其前置课程A或者B,那么若把功课ABC用图表示,然后进行拓扑排序,可以表示成为ABC或者BAC,总之C不能出现在A或者B的前面。由此可知,很多时候拓扑排序不止一个结果。一般而言,只需要一个结果就够了。
假设图有V个顶点(vetex)和E条边(edge),那么拓扑排序的时间和空间复杂度均为O(|V|+|E|)。
拓扑排序一般有两种方法,即BFS和DFS方法。

  1. BFS
    拓扑排序的BFS方法,步骤如下:
    I.遍历图,计算每个节点的入度(in order/degree),所谓入度,就是有几个其他节点有进入这个节点的边。很明显,入度为0的节点,没有前驱节点,因此在拓扑排序里面应该放在最前面。假如有好几个这种0入度的顶点,它们的顺序是无所谓的。这里把它们放入队列,同时也放入结果数组。
    II.逐个删除队列里的0入度点,然后对于和其相连的顶点,入度-1。其实就相当于把这些顶点从图上面删掉。
    III.再次检查图,把新的0入度的点加入队列和结果。
    IV.重复以上过程,直至图为空。
    可以看到,BFS方法就相当于每次把最外层的顶点给去除,一层一层剥下来。代码:
class DirectedGraphNode:
    def __init__(self, x):
        self.label = x
        self.neighbors = []

from collections import deque
class Solution:
    def topBFS(self, graph):
        countrd = {}
        for x in graph:
            countrd[x] = 0

        for i in graph:
            for j in i.neighbors:
                countrd[j] = countrd[j] + 1

        ans = []
        zeroDegree = deque()
        for i in graph:
            if countrd[i] == 0:
                ans.append(i)
                zeroDegree.append(i)

        while zeroDegree:
            i = zeroDegree.pop()
            countrd[i] -= 1
            for n in i.neighbors:
                countrd[n] -= 1
                if countrd[n] == 0:
                    ans.append(n)
                    zeroDegree.appendleft(n)

        return ans

2.DFS
DFS其实和BFS有相似的地方,也是围绕入度来实现,只不过是先一条线到底:

class DirectedGraphNode:
    def __init__(self, x):
        self.label = x
        self.neighbors = []

class Solution:
    def dfs(self, i, countrd, ans):
        ans.append(i)
        countrd[i] -= 1
        for j in i.neighbors:
            countrd[j] = countrd[j] - 1
            if countrd[j] == 0:
                self.dfs(j, countrd, ans)
    """
    @param graph: A list of Directedgraph node
    @return: A list of integer
    """
    def topSort(self, graph):
        # writeyour code here
        countrd= {}
        for x in graph:
            countrd[x] = 0

        for i in graph:
            for j in i.neighbors:
                countrd[j] = countrd[j] +1

        ans = []
        for i in graph:
            if countrd[i] == 0:
                self.dfs(i, countrd, ans)
        return ans

来看看实际应用:
1.选课问题I。假设有n门课要上,序号从0到n-1,假设用数组对来表示前置课程,例如用[0,1]来表示1是0的前置课程。现在给出n和前置课程对的数组,返回是否能够完成课程。
【解】相当于给出n个顶点的图和其边的信息,问是否能够对其进行拓扑排序。很明显关键就在于有没有环。
无论如何,第一步还是建立图的结构。因为没有直接给出neighbor信息,若再刻意地先求neighbor再按照之前的算法计算入度,就有点生硬。
其实,换一个做法,把边的信息分别存到入度表和出度表,效果还是一样的。之后还是按照正常的拓扑排序做法,只不过这里是把入度表为空的节点的出度表里的节点的入度表里的对应节点给删除。假设A是最外层的顶点,有AB这条边,那么B在A的出度表上,A在B的入度表上,因此要删除A,通过A的出度表找到B,然后再看B的入度表,把里面的A删掉。
如果有环,最后出度表会删不完全。因为环里面的元素天生不可能入度为空,那么自然不会从环上的节点开始,而从其他节点开始之后,删除到环,但环上的节点入度除了连接的非环节点以外,自然还是有环上的其他节点,因此怎么着也不会是空,所以就没法删除。

import collections
class Solution(object):
    def canFinish(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """
        zero_in_degree_queue, in_degree, out_degree = collections.deque(), {}, {}
        
        for i, j in prerequisites:
            if i not in in_degree:
                in_degree[i] = set()
            if j not in out_degree:
                out_degree[j] = set()
            in_degree[i].add(j)
            out_degree[j].add(i)
        
        for i in range(numCourses):
            if i not in in_degree:
                zero_in_degree_queue.append(i)
        
        while zero_in_degree_queue:
            prerequisite = zero_in_degree_queue.popleft()
            
            if prerequisite in out_degree:
                for course in out_degree[prerequisite]:
                    in_degree[course].discard(prerequisite)
                    if not in_degree[course]:
                        zero_in_degree_queue.append(course)
            
                del out_degree[prerequisite]
        
        if out_degree:
            return False
        
         return True 

2.选课问题II。接上题,返回拓扑排序的任意一个结果,假如没有,返回空数组。
【解】相似度99%,多加一个结果数组,把零入度的节点放进去即可。代码略。

3.外文字典。现在有一种新的外语,也用英文字母,但是顺序不一样。现在给出一组这种外文的单词,已经按照字典序排列,返回该外文的字母顺序。例如,给出[wrt,wrf,er,ett,rftt],返回wertf。假如有多个可能顺序,返回任一个即可。
【解】还是拓扑排序,只不过这次有多个对象,如何转化是一个关键问题。
容易犯的错误是把单词之间的字典序当做单词内的字母序,如果不仔细看例子,就会搞错。单词内不一定是按照字典序排列的,比如rftt,f就在t之前,题目只是说单词按照字典序排列而已。
也就是说,字母的顺序是在单词之间体现出来的,可以用这一点来比较相邻两个单词的字母,从而建立图。为什么不循环比较?没必要,两两比较已经够了。每次比较只能得出一个顺序,拿英文单词举例,az在bc之前,因此只能看出a<b,不能再看出z和c的大小。
例子当中,比较12,得出t<f,然后23,可知w<e,34知道r<t,45知道e<r,因此w<e<r<t<f。
之后的处理就还是老套路了,注意判断有没有环。代码如下:

# T: O(n) S:O(|V|+|E|)=O(1) 因为只有26个字母
import collections
# BFS solution.
class Solution(object):
    def alienOrder(self, words):
        """
        :type words: List[str]
        :rtype: str
        """
        result, zero_in_degree_queue, in_degree, out_degree = [], collections.deque(), {}, {}
        nodes = set()
        for word in words:
            for c in word:
                nodes.add(c)
        
        for i in range(1, len(words)):
            self.findEdges(words[i - 1], words[i], in_degree, out_degree)
        
        for node in nodes:
            if node not in in_degree:
                zero_in_degree_queue.append(node)
        
        while zero_in_degree_queue:
            precedence = zero_in_degree_queue.popleft()
            result.append(precedence)
            
            if precedence in out_degree:
                for c in out_degree[precedence]:
                    in_degree[c].discard(precedence)
                    if not in_degree[c]:
                        zero_in_degree_queue.append(c)
            
                del out_degree[precedence]
        
        if out_degree:
            return ""

        return "".join(result)


    # Construct the graph.
    def findEdges(self, word1, word2, in_degree, out_degree):
        str_len = min(len(word1), len(word2))
        for i in range(str_len):
            if word1[i] != word2[i]:
                if word2[i] not in in_degree:
                    in_degree[word2[i]] = set()
                if word1[i] not in out_degree:
                    out_degree[word1[i]] = set()
                in_degree[word2[i]].add(word1[i])
                out_degree[word1[i]].add(word2[i])
                break 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 1 序 2016年6月25日夜,帝都,天下着大雨,拖着行李箱和同学在校门口照了最后一张合照,搬离寝室打车去了提前租...
    RichardJieChen阅读 5,016评论 0 12
  • 课程介绍 先修课:概率统计,程序设计实习,集合论与图论 后续课:算法分析与设计,编译原理,操作系统,数据库概论,人...
    ShellyWhen阅读 2,156评论 0 3
  • 现实生活中有很大一类问题可以用简洁明了的图论语言来描述,可以转化为图论问题。 相关定义 图可以表示为G=(V, E...
    芥丶未央阅读 1,561评论 0 7
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,660评论 0 19
  • 这是2017年阅读的第11本书,离150本还有139本,连续每日读书第5天。 17.1.13读书感悟——《疯传》 ...
    吞书兽小布阅读 188评论 0 0