【BFS 解决拓扑排序】火星词典

文章目录

题目链接:LCR 114. 火星词典


题目解析

拓扑排序 (Topological sorting )要解决的问题是 如何给一个有向无环图的所有节点排序。

有向无环图 (Directed Acyclic Graph, 缩写 DAG),是一种 边有方向且没有环形结构的图结构。

拓扑排序的起始点是 顶点(入度为0的节点),将顶点排序完毕后,从该顶点发出的有向边全部会被删除,这使得被指向的节点的入度数会减1,当入度数为0时这个节点就成为新了的顶点,然后下一轮从该顶点开始排序,以此重复,直到没有顶点为止。

  • 入度:表示指向某节点的有向边数量,
  • 出度:表示从该节点发出的有向边数量

构造拓扑序列步骤:

  1. 从图中选择一个入度为零的点
  2. 输出该顶点,从图中删除此顶点及其所有的出边

重复上面两步,直到所有顶点都输出,拓扑排序完成,或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成,陷入死锁。

例子:做一道 青椒炒肉 的过程图如下

按照流程图中的顺序一步一步做出来这道菜的过程,就相当于是 拓扑排序 的过程。

拓扑排序的目标是将所有节点排序,可能出现多个不同的排序结果:

  • 买菜 ---> 准备厨具 ---> 洗菜 ---> 腌肉 ---> 切菜 ---> 炒菜 ---> 装盘
  • 准备厨具 ---> 买菜 ---> 洗菜 ---> 腌肉 ---> 切菜 ---> 炒菜 ---> 装盘
  • 准备厨具 ---> 买菜 ---> 洗菜 ---> 切菜 ---> 腌肉 ---> 炒菜 ---> 装盘
  • 准备厨具 ---> 腌肉 ---> 买菜 ---> 洗菜 ---> 切菜 ---> 炒菜 ---> 装盘

如何实现拓扑排序?借助队列,一次多源 BFS 即可:

  1. 先将所有入度为0的节点放入队列
  2. 层序遍历:先拿出队首元素并添加到结果中,然后将从该队首元素发出的边全部删去,再判断与这些边相连的节点的入度减一之后的值是否为0(若为0就再加入队列中)

题目给出一个字符串列表 words,作为一门使用英语字母但字典顺序不同的外星文语言的 词典,这个词典中的字符串已经是有序的(按照外星文语言的字典顺序排序)。

我们需要根据这个词典还原出这种语言中已知的字母顺序,并 按照字母递增顺序 排列。若不存在合法字母顺序就返回 ""。若存在多种可能的合法字母顺序,返回其中 任意一种 序列即可。

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

  1. 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
  2. 如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。

例1:words = ["wrt","wrf","er","ett","rftt"]

将 wrt 分别与 wrf、er、ett 和 rftt进行对比,然后分别对其余字符串一一比较:

可以得到有向无环图:

对这个有向无环图进行拓扑排序可以得到:w -> e -> r -> t -> f,最终的序列是:wertf。

例2:words = ["z","x","z"]

由于不存在合法的字母顺序,返回 ""。


算法原理

要解决这道题,分为以下几个步骤:

  1. 先统计信息(即根据 words 中的字符串得出字母的字典顺序),这一步同时也进行建图
  2. 题目要求我们返回字典顺序,相当于建图完成之后我们需要返回这张图的拓扑序列,因此接下来是 拓扑排序:先遍历入度数组(在建图时就存入所有节点的入度值),将入度为0的节点放入队列中;然后层序遍历,逐一取出队首元素并加入到结果序列中,同时要将所有被队首元素所指的节点的入度值减一,然后需要判断该节点的入度值是否为0,若为0就要加入到队列中
  3. 当拓扑排序结束后,此时图中所有节点的入度值应都是0,如果还有节点的入度不为0,说明图中有环------即无法得出有效的字典顺序,返回 "" 即可;若全部节点的入度都是0,就可以返回结果序列。

建图

在构造拓扑序列之前,首先要根据题目抽象出图结构。

我们用 邻接表 来表示图的结构,代码实现有两种方式:

  • 对于字符串类型的数据,我们通常用 哈希表 (Map < String, List < String >> edges)作为邻接表来映射 "节点,相连的节点列表"。
  • 对于整数类型的数据,我们通常用 链表(List< List< Integer >> edges)作为邻接表,用下标(前提是数据从0开始计数)作为 "节点",链表的值作为 "与该节点相连的节点列表"。(也可以使用哈希表来映射:Map < Integer, List < Integer >> edges)
  • 本题的邻接表采用哈希表来实现,并且可能会出现重复情况(比如例1中比较 wrt 与 er 和比较 wrt 与 ett 这两种情况都能得到 w -> e,此时出现重复),而邻接表中不希望出现重复的情况,此时我们将哈希表的结构改成:Map < Character , Set < Character > >。
java 复制代码
Map<Character, Set<Character>> edges = new HashMap<>();

本题的建图是在统计信息的时候一同进行的,具体的过程:

  1. 先遍历词典 words,将其中出现的所有字符串相互进行比较(使用双指针,逐个字符遍历),可以得出字母的先后顺序(如例1中的 wrt 与 wrf,这两个只有最后一个字符不相同,于是得出字典序 t 在 f 前,抽象成图则是 t -> f)。
  2. 当两个字符串的字符不相等,我们就得到一组信息,然后将这组信息存入邻接表 edges 中,然后直接结束本轮比较即不需要继续比较这两个字符串了

入度数组

本题我们在进行拓扑排序的过程中还需要知道节点的入度值,因此我们为了方便,可以用一个入度数组 in 来记录图中所有节点的入度值。

java 复制代码
int[] in = new int[numCourses];

但是,由于本题无法事先知道会出现哪些字母,而且直接开辟一个长度为 26 的数组也太过浪费,因此我们将入度数组的结构也改成哈希表:Map < Character, Integer >,存储特定的字母及其入度值的映射。

java 复制代码
Map<Character, Integer> in = new HashMap<>();
  • 注意:使用哈希表结构必须要在使用前初始化!否则会报错。
  • 具体的初始化操作:遍历题目所给的字符串列表 words,然后将里面出现的所有字母都添加进入度数组,对应的入度值设置为0

细节问题

我们需要针对一种特殊情况进行处理,下面给出一个例子:str1 = "abc",str2 = "ab"。

当我们对这两个字符串进行比较,并不会认为这是个非法情况,原因是我们比较的时候为了不越界会取较短的字符串长度,遍历到字符 b 就会停止。

但是题目明确给出:字符串 s 与 t,如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。 也就是说当两个字符串的前面 n 个字母都相同时,前一个字符串的长度只有小于后一个字符串的长度才合法,此时字符串 s 的字典顺序小于字符串 t。

对于我们这个例子,"abc" 和 "ab" 的比较只会比较 2 次,并且两个字符串的 "ab" 字母都相同,但是 "abc" > "ab",因此这个例子不合法,我们应该返回 ""。

解决的办法:

在结束两个字符串的比较之后,我们需要特别判断一下当前用于比较的指针的位置是否等于 str2 的长度,同时这个位置还比 str1 的长度小,若都满足就是上面例子的情况,直接返回 ""。


代码实现

java 复制代码
class Solution {
    public String alienOrder(String[] words) {
        Map<Character, Set<Character>> edges = new HashMap<>(); 
        Map<Character, Integer> in = new HashMap<>(); // 记录入度值
        // 初始化,将words中所有单词的字符记录下来,入度均为0
        for (String word : words) {
            for (int j = 0; j < word.length(); j++) {
                char ch = word.charAt(j);
                in.put(ch, 0);
            }
        }

        // 遍历收集信息
        int len = words.length;
        for (int i = 0; i < len; i++) {
            char[] word = words[i].toCharArray();
            for (int j = i+1; j < len; j++) {
                char[] nextWord = words[j].toCharArray();
                int n1 = word.length, n2 = nextWord.length;
                // 比较两个字符串
                int n = Math.min(n1, n2);
                int k = 0; 
                for (; k < n; k++) {
                    char a = word[k], b = nextWord[k];
                    if (a != b) {
                        // a在b之前,即:a -> b
                        if (!edges.containsKey(a)) {
                            edges.put(a, new HashSet<>());
                        }
                        if (!edges.get(a).contains(b)) { // 防止重复
                            edges.get(a).add(b);    // 存入邻接表
                            in.put(b, in.get(b)+1); // 更新b的入度值
                        }
                        // 有一个字符不相同后面就不继续比较了
                        break;
                    }
                }
                // 细节问题
                if (k == n2 && k < n1) {
                    // 不合法
                    return "";
                }
            }
        }

        // 拓扑排序
        Queue<Character> queue = new LinkedList<>();
        // 把入度为0的字符加入队列
        for (char ch : in.keySet()) {
            // 找到入度值为0的字符
            if (in.get(ch) == 0) {
                // 放入队列
                queue.offer(ch);
            }
        }
        // 层序遍历
        StringBuilder ans = new StringBuilder();
        while (!queue.isEmpty()) {
            char top = queue.poll(); // 取出队首元素
            ans.append(top);
            for (char ch : edges.getOrDefault(top, new HashSet<>())) {
                // 将与队首元素相连的字符的入度值减一
                in.put(ch, in.get(ch)-1);
                // 判断减一之后的入度值是否为0,若为0则加入队列
                if (in.get(ch) == 0) {
                    queue.offer(ch);
                }
            }
        }

        // 判断是否还有入度不为0的字符,若有就返回空字符串,否则返回结果
        for (char ch : in.keySet()) {
            if (in.get(ch) != 0) {
                // 返回空字符串
                return "";
            }
        }
        // 返回结果
        return ans.toString();
    }
}

完

相关推荐
霸道流氓气质1 小时前
Java调用StableDiffusion/ComfyUI图像生成:DAG工作流编排、WebSocket实时进度与Flux模型集成的生产级实战
java·websocket·stable diffusion
北风toto1 小时前
KMP 算法详解:从暴力匹配到 O(n+m) 的字符串搜索
java·开发语言·算法
weixin199701080161 小时前
《京东JOS API:落地方案、接口边界与业务踩坑 —— 联盟免费 vs 商家按量的双轨制》(附python源码)
java·网络·python
衡石科技1 小时前
从语义准备到模型验收:HENGSHI SENSE Data Agent 的 RAG 与部署工程实践
java·大数据·人工智能·数据分析·rag
G018_star sky♬1 小时前
java学习日记-多态与好玩的写法
java
林澈在路上1 小时前
哪个写歌软件可以用自己声音?MELO音乐详解
人工智能·python·算法
撸串-研究生1 小时前
面向景区的综合服务管理平台设计——以稻城亚丁景区为例【附源码】
java·开发语言·vue.js·前后端分离·稻城亚丁景区
爱读书的小胖1 小时前
Codewars Sum of a Sequence Hard Core Version C 解题分析
c语言·数据结构·算法
不会就选b1 小时前
算法日常・每日刷题--<动态规划>24
算法·动态规划