题目描述

✅ LCR 114. 火星词典

image-20260929004652222

image-20260929004652223

题意分析

单词已经按未知字母顺序排好,要求返回输入中出现过的全部字符的一种合法顺序。多个答案时任意一个即可;若单词次序无法由任何字母顺序解释,返回空字符串。

解法:相邻词首个差异建图与拓扑排序

核心思路

[!blue]

字典序由两个单词的第一处不同字符决定。比较相邻单词 w1、w2,找到首个差异位置 j 后,只能推出 w1[j] 排在 w2[j] 之前,建立这一条有向边;后续字符不再提供顺序约束。

若较短单词耗尽仍没有差异,两者相同或互为前缀。前词更长时,它排在了自己的严格前缀之前,无论怎么安排字母都不合法,应立即返回空字符串;其他情况不需要加边。检查相邻单词已经足够,因为它们全部满足顺序后,整个列表也按字典序有序。

先注册所有出现过的字符,再建约束图,才能保留没有边的孤立字符。代码用集合保存后继,同一条边只存一次,因此也只能在成功加入新边时增加目标字符的入度,保证增加与后续删除的次数对应。

建图后进行拓扑排序。indegree[ch] 为尚未输出的前驱数量,所有零入度字符入队;每次输出一个字符,就将其后继入度减一,减到 0 时加入队列。每个被输出的字符都排在所有前驱之后,因此所得顺序满足全部相邻单词约束。

最后必须检查输出长度是否等于已出现的字符数。若不足,剩余图中存在环,字符先后关系相互矛盾;若相等就返回结果。前缀矛盾不一定产生任何边,必须在建图时单独检查,不能只依靠拓扑判环。

解题步骤

  1. 遍历所有字符,初始化邻接集合和入度表。
  2. 逐对比较相邻单词,定位第一处不同字符。
  3. 没有差异且前词更长时返回空串;有差异时加入对应有向边,仅对新边增加入度。
  4. 将全部零入度字符入队,依次输出,并沿出边解除后继约束。
  5. 输出覆盖全部已知字符时返回答案,否则返回空串。

代码实现

class Solution {
    // 如果前一个单词更长且完全包含后一个单词,输入顺序本身非法,必须返回空字符串。
    public String alienOrder(String[] words) {
        Map<Character, Set<Character>> graph = new HashMap<>();
        Map<Character, Integer> indegree = new HashMap<>();

        for (String word : words) {
            for (char ch : word.toCharArray()) {
                graph.putIfAbsent(ch, new HashSet<>());
                indegree.putIfAbsent(ch, 0);
            }
        }

        for (int i = 0; i < words.length - 1; i++) {
            String w1 = words[i];
            String w2 = words[i + 1];
            int len = Math.min(w1.length(), w2.length());
            int j = 0;

            while (j < len && w1.charAt(j) == w2.charAt(j)) {
                j++;
            }

            if (j == len && w1.length() > w2.length()) {
                return "";
            }

            if (j < len) {
                char from = w1.charAt(j);
                char to = w2.charAt(j);

                if (graph.get(from).add(to)) {
                    indegree.put(to, indegree.get(to) + 1);
                }
            }
        }

        Queue<Character> queue = new ArrayDeque<>();

        for (char ch : indegree.keySet()) {
            if (indegree.get(ch) == 0) {
                queue.offer(ch);
            }
        }

        StringBuilder sb = new StringBuilder();

        while (!queue.isEmpty()) {
            char cur = queue.poll();

            sb.append(cur);

            for (char next : graph.get(cur)) {
                indegree.put(next, indegree.get(next) - 1);

                if (indegree.get(next) == 0) {
                    queue.offer(next);
                }
            }
        }

        if (sb.length() != indegree.size()) {
            return "";
        }

        return sb.toString();
    }
}
func alienOrder(words []string) string {
    // 如果前一个单词更长且完全包含后一个单词,输入顺序本身非法,必须返回空字符串。
    graph := make(map[byte]map[byte]struct{})
    indegree := make(map[byte]int)
    for _, w := range words {
        for i := 0; i < len(w); i++ {
            ch := w[i]
            if _, ok := graph[ch]; !ok {
                graph[ch] = make(map[byte]struct{})
            }
            if _, ok := indegree[ch]; !ok {
                indegree[ch] = 0
            }
        }
    }
    for i := 0; i < len(words)-1; i++ {
        w1 := words[i]
        w2 := words[i+1]
        minLen := len(w1)
        if len(w2) < minLen {
            minLen = len(w2)
        }
        j := 0
        for j < minLen && w1[j] == w2[j] {
            j++
        }
        if j == minLen && len(w1) > len(w2) {
            return ""
        }
        if j < minLen {
            from := w1[j]
            to := w2[j]
            if _, ok := graph[from][to]; !ok {
                graph[from][to] = struct{}{}
                indegree[to]++
            }
        }
    }

    queue := make([]byte, 0)
    for ch, deg := range indegree {
        if deg == 0 {
            queue = append(queue, ch)
        }
    }

    order := make([]byte, 0, len(indegree))
    for len(queue) > 0 {
        cur := queue[0]
        queue = queue[1:]
        order = append(order, cur)
        for next := range graph[cur] {
            indegree[next]--
            if indegree[next] == 0 {
                queue = append(queue, next)
            }
        }
    }

    if len(order) != len(indegree) {
        return ""
    }
    return string(order)
}

复杂度分析

  • 时间复杂度:$O(C+V+E)$,C 为总字符数,V 为不同字符数,E 为去重后的边数。每个单词最多参与前后两次相邻比较;拓扑排序各处理一次节点和边。
  • 空间复杂度:$O(V+E)$,保存约束图、入度和队列。字符集只有 26 个小写字母,图的规模有固定上限。

关键点总结

[!green]

  • 只从相邻单词的首个差异提取顺序,不能继续比较后面的字符来加边。
  • 长词在其严格前缀之前与约束成环是两种不同的无解原因。
  • 所有出现过的字符都要输出,包括不参与任何边的字符。
  • 邻接集合已经去重时,入度也必须只计算不同的边;多个零入度字符可任选一个先输出。

易错点总结

[!yellow]

  • 先注册全部出现的字符,孤立字符也应进入答案。
  • 相邻单词只由第一处不同字符产生一条约束;长词排在其严格前缀之前时直接无解。
  • 邻接集合去重时,入度也只对新边加一;最后检查输出覆盖全部字符。

相似题目

题目 难度 关联与区别
953. 验证外星语词典 简单 原题给定字母顺序后验证词典,本题从相邻单词的第一个不同字符反推先后约束。
210. 课程表 II 中等 推导字母边之后,复用拓扑排序输出一个符合依赖关系的顺序。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/95537875
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!