【优选算法】专题十八——BFS解决拓扑排序问题

文章目录

拓扑排序简介

有向无环图(DAG图)

什么是有向无环图?

  • 像上面这样由若干节点组成的图,且节点的走向是有方向性的(节点间的指向),但是节点的走向又不能成环,这样的图就成为有向无环图。
  • 对于每个节点指向该节点的箭头的数量称为该节点的入度(重要),从该节点出发指向其他节点的箭头的数量称为该节点的出度

顶点活动图(AOV图)

  • 在有向无环图的基础上,每个节点代表一个活动(事件),用箭头(边)来表示执行各活动的先后顺序(比如A--->B代表执行B活动之前必须先执行A活动),这样的图称为顶点活动图。

拓扑排序

  • 对于一个顶点活动图,我们发现可以由图的结构(节点间的指向)得出所有事件执行的先后顺序,这就叫做拓扑排序。
  • 但拓扑排序的结果可能是不唯一的,比如上图的"辣椒炒肉的工程图"中"准备餐具"和"买菜"这两个事件先后顺序随意。

那么对于这样一个顶点活动图,我们应该怎么找出排序结果呢?

下面给出大致的排序思路:

  1. 找到图中入度为0的点,输出
  2. 处理完一个点之后,删除该点对其他点的指向,也就是它所指向的所有节点的度都要 -1
  3. 重复1、2操作,直到图中没有剩余的点了或者没有入度为0的点为止(因为顶点活动图会出现带环这样的非法情况,对于这些环中的节点我们无法进行删除操作,因为它们的入度不会变为0)

代码实现拓扑排序

  • 借助队列对上图来一次BFS即可:
  1. 初始化:把所有入度为0的点加入到队列中
  2. 当队列不为空的时候重复下面操作:
    (1) 拿出队头元素,处理该节点(比如,依次储存它们最终得出正确的事件执行顺序)
    (2) 删除与该节点相连的边(将该节点指向的所有节点的度 -1)
    (3) 判断:原来被指向的节点的入度 -1后是否变成0,如果入度为0,那就可以加入到队列中等待被处理了

最终得到(不唯一):

但以上操作都建立在外面首先要有一个这样的图形数据结构,所以我们要先用代码实现一个顶点活动图结构

如何建图?

灵活使用Java提供的容器

建立顶点活动图的方式是使用"邻接表"(两种方式):

  1. 使用HashMap:Map<Integer,List<Integer>> edges=new HashMap<>();
  • 其实我们并不用像二叉树那样建立一个指向清晰的图形结构,我们的做法是先将每个节点独立表示出来放到一块,不用管其互相的指向,但是要将该节点所指向的所有节点一并绑定与该节点储存。
  • 具体做法就是Map<Integer,List<Integer>>,key值为节点,value储存的就是该节点所指向的所有节点(放到一个容器中)
  • 还有就是要将每个节点的入度记录起来,我们单独找来一个数组(或者使用一个HashSet,它们各有适用性,在接下来的题目中体现),数组下标代表每个节点,每个位置储存该节点的入度,具体就是当我们得到:B--->A 这样的信息之后是不是就知道了A的入度要 +1了?(并且此时在Map<Integer,List<Integer>>中B就对应着key值,且A就要加入到value:List中了)
  1. 直接用List:List<List<Integer>> edges=new HashMap<>();
  • edges的下标来代表节点(也就是活动):0下标代表活动0、1下标代表活动1... ,对应下标位置的List储存的就是该节点所指向的所有节点
  • 那么也用同样的方法将节点的入度储存起来

由以上这些结构就可以使用上述的"借助队列来一次BFS"这个方法进行拓扑排序的实现了

一、课程表

Leetcode连接

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

解题思路

  • 判断是否可能完成所有课程的学习,就是判断是否可以正常对顶点活动图进行拓扑排序,那么如果图中有环结构就不能正常排序
  • 所以根据上面介绍的方法进行:收集活动执行顺序的信息(A--->B)来建图------>对图进行拓扑排序------>最后判断排序结果

代码实现及解析

java 复制代码
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int n=numCourses;
        Map<Integer,List<Integer>> edges=new HashMap<>();//用邻接表储存图
        int[] inDegree=new int[n];//存储入度信息(自动初始化为0)
        //建图(收集所有的像"A--->B"这样的信息)
        for(int[] infor:prerequisites){
            int B=infor[0],A=infor[1];//得到A--->B这样的信息
            if(!edges.containsKey(A)){
                edges.put(A,new ArrayList<>());
            }
            edges.get(A).add(B);//将A--->B信息放入邻接表中
            inDegree[B]++;//B入度+1
        }
        //把所有入度为0的节点先入队列
        Queue<Integer> que=new LinkedList<>();
        for(int i=0;i<inDegree.length;i++){
            if(inDegree[i]==0){
                que.offer(i);//别忘了inDegree的下标代表节点
            }
        }
        //开始BFS:
        while(!que.isEmpty()){
            int tmp=que.poll();//处理掉该节点
            //将该节点指向的所有节点的度-1
            for(int x:edges.getOrDefault(tmp,new ArrayList<>())){
                inDegree[x]--;
                if(inDegree[x]==0){//如果入度变为0,那就可以加入到队列中了
                    que.offer(x);
                }
            }
        }
        //要注意最后是用入度表来判断图是否带环的
        for(int x:inDegree){//处理完之后如果顶点活动图不带环(也就是可以将所有课程排序),那所有节点的入度应该都为0
            if(x!=0) return false;
        }
        
        return true;
    }
}

总结

  • 复习解题思路和代码实现及解析

二、LCR 114. 火星词典

Leetcode链接

现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。

给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序 。

请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。

字符串 s 字典顺序小于 字符串 t 有两种情况:

在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。

如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。

示例 1:

输入:words = ["wrt","wrf","er","ett","rftt"]

输出:"wertf"

解题思路

  • 当我们可以零零散散地获得一些事件的相对顺序,但要求得出所有事件的完整执行顺序(或判断是否可以完整执行所有事件)时,就应该想到这就是顶点活动图,使用拓扑排序来解决

  • 在本题中我们可以对于每一个单词组合得出一个字母顺序对(比如,a--->b),然后要求推导出所有出现的字母的完整字典序,就是典型的例子。

  • 这种题目就是要用拓扑排序来解决,要记住这种题目的特性及解决方法

代码实现及解析

java 复制代码
class Solution {
    Map<Character,List<Character>> edges=new HashMap<>();//邻接表
    Map<Character,Integer> inDegree=new HashMap<>();//储存每个节点的入度
    boolean check;//处理一种特殊情况的标记
    public String alienOrder(String[] words) {
        //将入度表初始化,不然开始那批入度为0的节点整个过程压根就不会出现在表中
        for(String word:words){
            for(int i=0;i<word.length();i++){
                if(!inDegree.containsKey(word.charAt(i))){
                    inDegree.put(word.charAt(i),0);
                }
            }
        }
        //1.收集信息并建图
        for(int i=0;i<words.length;i++){
            for(int j=i+1;j<words.length;j++){//每次检查两个单词进行信息的收集
                getInfor(words,i,j);//收集信息,在邻接表和入度表中更新对应信息
                if(check) return "";//words[i],words[j]这两个单词压根排序就不合法
            }
        }
        //2.将所有入度为0的节点先入队列
        Queue<Character> que=new LinkedList<>();
        for(char x:inDegree.keySet()){
            if(inDegree.get(x)==0){
                que.offer(x);
            }
        }
        //3.开始BFS
        StringBuilder ret=new StringBuilder();
        while(!que.isEmpty()){
            char tmpChar=que.poll();
            ret.append(tmpChar);//记录正确的外星字母顺序

            //断掉tmpChar所指向的这些所有节点的箭头(这些节点的入度-1)
            for(char t:edges.getOrDefault(tmpChar,new ArrayList<>())){
                inDegree.put(t,inDegree.get(t)-1);
                if(inDegree.get(t)==0) que.offer(t);//如果t的入度变为了0,那又可以将t入队列了
            }
        }
        
        //4.检查、判断
        for(int value:inDegree.values()){
            if(value!=0) return "";//说明仍存在入度不为0的节点,不符合条件
        }
        return ret.toString();
    }

    public void getInfor(String[] words,int i,int j){
        int n=Math.min(words[i].length(),words[j].length());
        String tmp1=words[i],tmp2=words[j];
        int index=0;//记录最终比较到了两个单词的哪一位
        while(index<n){
            char ch1=tmp1.charAt(index),ch2=tmp2.charAt(index);
            if(ch1!=ch2){//得到ch1--->ch2这样的信息
                if(!edges.containsKey(ch1)){
                    edges.put(ch1,new ArrayList<>());
                }
                if(!edges.get(ch1).contains(ch2)){//不同的二元单词组可能会获得相同的"ch1--->ch2"信息,不能重复记录
                    edges.get(ch1).add(ch2);//将信息ch1--->ch2存入邻接表中
                    inDegree.put(ch2,inDegree.get(ch2)+1);//ch2入度+1
                }
                break;//后面的字符就不能再处理了
            }
            index++;
        }
        //遍历结束,如果发现index在排序较后的words[j]中走到了头,但在位置较前的words[i]中却没走到头,
        //说明了明明前面遍历过程中字母都相等,但单词words[i]长度较长却排序在了前面,这压根就是不合法的,直接return ""
        if(index<words[i].length()&&index==words[j].length()) check=true;//标记遇到不合法单词排序
    }

}

总结

  • 复习解题思路和代码实现及解析
相关推荐
cui_ruicheng2 小时前
C++数据结构进阶:哈希表实现
数据结构·c++·算法·哈希算法·散列表
li星野2 小时前
[特殊字符] 模拟试卷一:C++核心与系统基础(90分钟)答案版
开发语言·c++·算法
二进制星轨2 小时前
leecode-283-移动零-算法题解
算法
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #215:数组中的第K个最大元素(快速选择、堆排序、计数排序等多种实现方案详解)
算法·leetcode·堆排序·快速选择·topk·数组中的第k个最大元素
2301_816651222 小时前
C++中的享元模式变体
开发语言·c++·算法
逆境不可逃2 小时前
LeetCode 热题 100 之 35. 搜索插入位置 74. 搜索二维矩阵 34. 在排序数组中查找元素的第一个和最后一个位置
数据结构·算法·leetcode
m0_583203132 小时前
C++中的访问者模式变体
开发语言·c++·算法
浅念-2 小时前
C ++ 智能指针
c语言·开发语言·数据结构·c++·经验分享·笔记·算法
不染尘.2 小时前
最小生成树算法
开发语言·数据结构·c++·算法·图论