LeetCode LCR114.火星词典

文章目录

问题描述

外星文字典(Alien Dictionary):给定一个按外星语言字母表顺序排序的单词列表,还原出外星语言的字母表顺序。

这是一个经典的图论 + 拓扑排序问题,广泛应用于任务调度、课程安排等场景。


核心算法:拓扑排序

这个问题本质上是拓扑排序问题。我们需要根据单词的排序关系,推导出字符之间的先后顺序。

关键概念

  1. 有向图:用邻接表表示字符之间的先后关系
  2. 入度:指向某个节点的边的数量,表示有多少个字符排在该字符之前
  3. 拓扑排序:对有向无环图(DAG)进行线性排序,使得对于任意有向边 u→v,u 在排序中都出现在 v 之前
  4. Kahn 算法:基于 BFS 的拓扑排序算法

算法步骤

第一步:初始化所有字符

python 复制代码
indegree = {}
for w in words:
    for c in w:
        if c not in indegree:
            indegree[c] = 0

目的:遍历所有单词中的每个字符,初始化入度为 0。

示例 :对于 words = ["wrt","wrf","er","ett","rftt"]

python 复制代码
indegree = {
    'w': 0,  # 来自 "wrt"
    'r': 0,  # 来自 "wrt"
    't': 0,  # 来自 "wrt"
    'f': 0,  # 来自 "wrf"
    'e': 0,  # 来自 "er"
}

第二步:构建有向图

python 复制代码
graph = defaultdict(list)
for i in range(len(words) - 1):
    cur = words[i]
    next_word = words[i + 1]
    j = 0
    length = min(len(cur), len(next_word))

    while j < length:
        if cur[j] != next_word[j]:
            graph[cur[j]].append(next_word[j])
            indegree[next_word[j]] += 1
            break
        j += 1

    if j < len(cur) and j == len(next_word):
        return ""

核心逻辑

  • 比较相邻的两个单词,找到第一个不同的字符
  • 如果 cur[j] != next_word[j],说明在外星语言中 cur[j] 应该排在 next_word[j] 之前
  • 建立一条有向边:cur[j] → next_word[j]
  • 同时增加 next_word[j] 的入度

为什么只比较第一个不同的字符?

  • 因为单词列表已经按字典序排列
  • 第一个不同的字符决定了两个单词的先后顺序
  • 后面的字符顺序无法从这两个单词中确定

特殊情况处理

python 复制代码
if j < len(cur) and j == len(next_word):
    return ""
  • 如果 next_wordcur 的前缀,但 cur 更长,则违反字典序规则
  • 例如:["abc", "ab"] 是不合法的,因为 "ab" 应该排在 "abc" 之前
建图过程详解(以测试用例 1 为例)

第 1 轮:比较 "wrt" 和 "wrf"

复制代码
cur = "wrt", next_word = "wrf"
j = 0: 'w' == 'w'  ✓ 相同
j = 1: 'r' == 'r'  ✓ 相同
j = 2: 't' != 'f'  ✗ 不同!

发现:t 应该排在 f 之前
操作:
  graph['t'].append('f')  → graph = {'t': ['f']}
  indegree['f'] += 1      → indegree['f'] = 1

第 2 轮:比较 "wrf" 和 "er"

复制代码
cur = "wrf", next_word = "er"
j = 0: 'w' != 'e'  ✗ 不同!

发现:w 应该排在 e 之前
操作:
  graph['w'].append('e')  → graph = {'t': ['f'], 'w': ['e']}
  indegree['e'] += 1      → indegree['e'] = 1

第 3 轮:比较 "er" 和 "ett"

复制代码
cur = "er", next_word = "ett"
j = 0: 'e' == 'e'  ✓ 相同
j = 1: 'r' != 't'  ✗ 不同!

发现:r 应该排在 t 之前
操作:
  graph['r'].append('t')  → graph = {'t': ['f'], 'w': ['e'], 'r': ['t']}
  indegree['t'] += 1      → indegree['t'] = 1

第 4 轮:比较 "ett" 和 "rftt"

复制代码
cur = "ett", next_word = "rftt"
j = 0: 'e' != 'r'  ✗ 不同!

发现:e 应该排在 r 之前
操作:
  graph['e'].append('r')  → graph = {'t': ['f'], 'w': ['e'], 'r': ['t'], 'e': ['r']}
  indegree['r'] += 1      → indegree['r'] = 1

建图完成后的状态

复制代码
graph(邻接表):
  't' → ['f']
  'w' → ['e']
  'r' → ['t']
  'e' → ['r']

indegree(入度):
  'w': 0  ← 没有字符排在 w 之前
  'r': 1  ← e 排在 r 之前
  't': 1  ← r 排在 t 之前
  'f': 1  ← t 排在 f 之前
  'e': 1  ← w 排在 e 之前

图结构可视化:
  w → e → r → t → f

第三步:拓扑排序(Kahn 算法)

python 复制代码
queue = deque()

# 将所有入度为 0 的字符加入队列
for c in indegree:
    if indegree[c] == 0:
        queue.append(c)

ans = []

# BFS 过程
while queue:
    cur = queue.popleft()
    ans.append(cur)
    
    for next_char in graph[cur]:
        indegree[next_char] -= 1
        if indegree[next_char] == 0:
            queue.append(next_char)

算法原理

  1. 入度为 0 的字符表示没有前驱,可以作为起点
  2. 每次取出一个入度为 0 的字符,加入结果列表
  3. 删除该字符发出的所有边(将后续字符的入度减 1)
  4. 如果某个字符的入度变为 0,说明它的所有前驱都已处理完毕,加入队列
  5. 重复上述过程,直到队列为空
拓扑排序详细过程(测试用例 1)
python 复制代码
words1 = ["wrt", "wrf", "er", "ett", "rftt"]
print(alienOrder(words1))  # 输出:"wertf"

初始化队列

python 复制代码
queue = deque(['w'])  # 只有 'w' 的入度为 0
ans = []

第 1 轮 BFS

复制代码
queue = ['w']
ans = []

1. cur = queue.popleft() = 'w'
2. ans.append('w')  → ans = ['w']
3. 遍历 graph['w'] = ['e']:
   - indegree['e'] -= 1  → indegree['e'] = 0
   - indegree['e'] == 0,加入队列
   - queue.append('e')  → queue = ['e']

状态:queue = ['e'], ans = ['w']

第 2 轮 BFS

复制代码
queue = ['e']
ans = ['w']

1. cur = queue.popleft() = 'e'
2. ans.append('e')  → ans = ['w', 'e']
3. 遍历 graph['e'] = ['r']:
   - indegree['r'] -= 1  → indegree['r'] = 0
   - indegree['r'] == 0,加入队列
   - queue.append('r')  → queue = ['r']

状态:queue = ['r'], ans = ['w', 'e']

第 3 轮 BFS

复制代码
queue = ['r']
ans = ['w', 'e']

1. cur = queue.popleft() = 'r'
2. ans.append('r')  → ans = ['w', 'e', 'r']
3. 遍历 graph['r'] = ['t']:
   - indegree['t'] -= 1  → indegree['t'] = 0
   - indegree['t'] == 0,加入队列
   - queue.append('t')  → queue = ['t']

状态:queue = ['t'], ans = ['w', 'e', 'r']

第 4 轮 BFS

复制代码
queue = ['t']
ans = ['w', 'e', 'r']

1. cur = queue.popleft() = 't'
2. ans.append('t')  → ans = ['w', 'e', 'r', 't']
3. 遍历 graph['t'] = ['f']:
   - indegree['f'] -= 1  → indegree['f'] = 0
   - indegree['f'] == 0,加入队列
   - queue.append('f')  → queue = ['f']

状态:queue = ['f'], ans = ['w', 'e', 'r', 't']

第 5 轮 BFS

复制代码
queue = ['f']
ans = ['w', 'e', 'r', 't']

1. cur = queue.popleft() = 'f'
2. ans.append('f')  → ans = ['w', 'e', 'r', 't', 'f']
3. 遍历 graph['f'] = [] (空列表)
   - 无操作

状态:queue = [], ans = ['w', 'e', 'r', 't', 'f']

第四步:检查是否存在环

python 复制代码
return ''.join(ans) if len(ans) == len(indegree) else ""

判断逻辑

  • 如果 len(ans) == len(indegree),说明所有字符都被访问到,拓扑排序成功
  • 否则说明图中存在,无法形成合法的字典序,返回空字符串

为什么能检测环?

  • 如果存在环(如 a→b→c→a),环中的字符入度永远无法变为 0
  • 这些字符永远不会被加入队列
  • 最终 ans 的长度会小于字符总数

完整代码(带详细注释)

python 复制代码
class Solution:
    def alienOrder(self, words: List[str]) -> str:
        """
        外星文字典问题
        给定一个按外星语言字母表顺序排序的单词列表,还原出外星语言的字母表顺序

        参数:
            words: 按外星语言字典序排列的单词列表

        返回:
            外星语言的字母表顺序字符串,如果不存在合法顺序则返回空字符串
        """
        # graph: 邻接表表示的有向图,key 是字符,value 是该字符指向的所有字符列表
        # 例如:graph['w'] = ['e'] 表示 w 在 e 之前
        graph = defaultdict(list)

        # indegree: 记录每个字符的入度(有多少个字符排在该字符之前)
        indegree = {}

        # 第一步:初始化,将所有出现的字符都加入入度表,初始入度为 0
        for w in words:
            for c in w:
                if c not in indegree:
                    indegree[c] = 0

        # 第二步:构建有向图
        # 通过比较相邻的两个单词,找出字符之间的先后关系
        for i in range(len(words) - 1):
            cur = words[i]      # 当前单词
            next_word = words[i + 1]  # 下一个单词
            j = 0
            # 取两个单词中较短的长度,用于逐字符比较
            length = min(len(cur), len(next_word))

            # 逐字符比较,找到第一个不同的字符
            while j < length:
                if cur[j] != next_word[j]:
                    # 找到第一个不同的字符,说明 cur[j] 应该排在 next_word[j] 之前
                    # 添加一条从 cur[j] 指向 next_word[j] 的有向边
                    graph[cur[j]].append(next_word[j])
                    # next_word[j] 的入度加 1
                    indegree[next_word[j]] += 1
                    # 只需要比较到第一个不同的字符即可,后面的字符顺序无法确定
                    break
                j += 1

            # 特殊情况处理:如果前面的字符都相同,但前一个单词更长
            # 例如:["abc", "ab"] 是不合法的字典序,因为 "ab" 应该排在 "abc" 之前
            # 当 j == len(next_word) 时,说明 next_word 是 cur 的前缀
            # 此时如果 cur 更长 (j < len(cur)),则违反了字典序规则,返回空字符串
            if j < len(cur) and j == len(next_word):
                return ""

        # 第三步:拓扑排序(使用 Kahn 算法)
        # 初始化队列,将所有入度为 0 的字符加入队列
        # 入度为 0 表示没有字符排在该字符之前,可以作为起点
        queue = deque()

        for c in indegree:
            if indegree[c] == 0:
                queue.append(c)

        # 存储拓扑排序结果
        ans = []

        # BFS 过程:不断从队列中取出入度为 0 的字符
        while queue:
            # 取出队首字符
            cur = queue.popleft()
            # 加入结果列表
            ans.append(cur)

            # 遍历该字符指向的所有后续字符
            for next_char in graph[cur]:
                # 将后续字符的入度减 1(相当于删除当前字符发出的边)
                indegree[next_char] -= 1
                # 如果后续字符的入度变为 0,说明它的所有前驱都已处理完毕
                # 将其加入队列
                if indegree[next_char] == 0:
                    queue.append(next_char)

        # 第四步:检查是否存在环
        # 如果结果列表的长度等于字符总数,说明所有字符都被访问到,拓扑排序成功
        # 否则说明图中存在环,无法形成合法的字典序,返回空字符串
        return ''.join(ans) if len(ans) == len(indegree) else ""

复杂度分析

时间复杂度:

  • C 是所有单词中字符的总数
  • 初始化字符:O©
  • 构建图:O©,需要遍历所有字符
  • 拓扑排序:O(V + E),其中 V 是字符数(最多 26),E 是边数(最多 C)
  • 总体:O©

空间复杂度:O(1)

  • 字符集大小固定(26 个小写字母)
  • 图和入度表的空间都是常数级别
  • 严格来说是 O(Σ),其中 Σ 是字符集大小

关键知识点总结

1. 拓扑排序的应用场景

  • 任务调度:某些任务必须在其他任务之前完成
  • 课程安排:某些课程是其他课程的先修课
  • 依赖解析:如包管理器中的依赖关系
  • 编译器:指令调度、依赖分析

2. Kahn 算法的核心思想

  • 维护所有入度为 0 的节点
  • 每次取出一个入度为 0 的节点,删除其发出的所有边
  • 如果新产生入度为 0 的节点,加入队列
  • 重复直到没有入度为 0 的节点

3. 环检测的方法

  • 如果拓扑排序无法包含所有节点,说明存在环
  • 也可以使用 DFS + 三色标记法检测环

4. 字典序的性质

  • 比较两个单词时,第一个不同的字符决定先后顺序
  • 如果单词 A 是单词 B 的前缀,则 A 应该排在 B 之前

相关推荐
Frostnova丶2 小时前
LeetCode 1415. 长度为 n 的开心字符串中字典序第 k 小的字符串
数据结构·算法·leetcode
美好的事情能不能发生在我身上2 小时前
Leetcode热题100中的:技巧专题
算法·leetcode·职场和发展
x_xbx2 小时前
LeetCode:53. 最大子数组和
算法·leetcode·职场和发展
Sakinol#3 小时前
Leetcode Hot 100 ——回溯part01
算法·leetcode
一叶落4383 小时前
LeetCode 137. 只出现一次的数字 II —— 位运算解法
c语言·数据结构·算法·leetcode·哈希算法
阿豪只会阿巴3 小时前
咱这后续安排
c++·人工智能·算法·leetcode·ros2
逆境不可逃3 小时前
LeetCode 热题 100 之 215. 数组中的第K个最大元素 347. 前 K 个高频元素 295. 数据流的中位数
算法·leetcode·职场和发展
abant24 小时前
leetcode 84 单调栈
算法·leetcode·职场和发展
TracyCoder1234 小时前
LeetCode Hot100(63/100)——31. 下一个排列
数据结构·算法·leetcode