LeetCode 269困难高级图
火星词典 图解题解
这道题到底在问什么
给按外星字典序排好的单词,推断字符顺序。
- 示例
- ["wrt","wrf","er","ett","rftt"] → "wertf"
最优解:一步一步想明白
- 3直接统计字符频率和顺序无关。
- 4比较相邻单词第一个不同字符,得到有向边;再拓扑排序。
- 5差异位: t < f相邻两词 wrt 和 wrf 前两位相同,第 3 位 t≠f → 字典序里 t 排在 f 前 → 连一条有向边 t→f。
- 6差异位: w < e下一对 wrf 和 er,第 1 位就不同:w≠e → 边 w→e。每对相邻词只贡献第一个不同字符那一条边。
- 7边: t→f · w→e · r→t · e→r再比 er/ett 得 r→t、ett/rftt 得 e→r。四条边连成一条链。入度:w=0,e/r/t/f 各=1。入度 0 的点没有任何字母必须排在它前面。
- 8结果: w拓扑排序:w 入度 0,第一个进结果;删掉 w 的出边后,e 的入度降到 0,成为下一个候选。
- 9结果: w e r t f沿链一路出队:w → e → r → t → f。拼起来就是这门火星语的字母表顺序 "wertf"。
- 12一句话记住:比较相邻单词第一个不同字符,得到有向边;再拓扑排序。
- 15环 = 矛盾约束 → 返回 ""雷区:若约束里出现环(比如又推出 e→w),说明字母顺序自相矛盾,拓扑排序会有点排不出来 → 返回空串 ""。无环才有唯一/合法顺序。
- 22这题学完不要乱跳,下一步去对应专题 /leetcode-animation/ds?k=graph 连刷同类题;卡住时用 AI 答疑问“我的状态定义哪里不稳”。
⚠️ 容易写错的地方
✗ 错:忽略「长词排在其前缀短词之前」的非法输入
✓ 对:比相邻两词时,若较短词先到末尾仍全等且前者更长(如 abc 排在 ab 前)→直接判无解
abc 是 ab 的扩展却排在前面违反字典序,推不出任何边、必须返回空串;漏判会得到错误顺序。
✗ 错:边界输入不单独跑
✓ 对:先跑空/单元素/重复/极端值
这些输入最容易暴露初始化和越界问题。
完整代码(Python / C++ / Java)
Python
class Solution:
def alienOrder(self, words):
from collections import defaultdict, deque
graph = {ch: set() for w in words for ch in w}
indeg = {ch: 0 for ch in graph}
for a, b in zip(words, words[1:]):
if len(a) > len(b) and a.startswith(b):
return ""
for x, y in zip(a, b):
if x != y:
if y not in graph[x]:
graph[x].add(y)
indeg[y] += 1
break
q = deque([ch for ch in indeg if indeg[ch] == 0])
ans = []
while q:
ch = q.popleft()
ans.append(ch)
for nxt in graph[ch]:
indeg[nxt] -= 1
if indeg[nxt] == 0:
q.append(nxt)
return "".join(ans) if len(ans) == len(indeg) else ""C++
class Solution {
public:
string alienOrder(vector<string>& words) {
unordered_map<char, unordered_set<char>> g;
unordered_map<char, int> indeg;
for (auto& w : words) for (char ch : w) indeg[ch] = 0;
for (int i=0;i+1<words.size();i++) {
string a = words[i], b = words[i+1];
if (a.size() > b.size() && a.substr(0, b.size()) == b) return "";
for (int j=0;j<min(a.size(), b.size());j++) if (a[j] != b[j]) {
if (!g[a[j]].count(b[j])) { g[a[j]].insert(b[j]); indeg[b[j]]++; }
break;
}
}
queue<char> q;
for (auto& [ch, deg] : indeg) if (deg == 0) q.push(ch);
string ans;
while (!q.empty()) {
char ch = q.front(); q.pop(); ans += ch;
for (char nxt : g[ch]) if (--indeg[nxt] == 0) q.push(nxt);
}
return ans.size() == indeg.size() ? ans : "";
}
};Java
class Solution {
public String alienOrder(String[] words) {
Map<Character, Set<Character>> g = new HashMap<>();
Map<Character, Integer> indeg = new HashMap<>();
for (String w: words) for (char ch: w.toCharArray()) indeg.putIfAbsent(ch, 0);
for (int i=0;i+1<words.length;i++) {
String a=words[i], b=words[i+1];
if (a.length()>b.length() && a.startsWith(b)) return "";
for (int j=0;j<Math.min(a.length(), b.length());j++) if (a.charAt(j)!=b.charAt(j)) {
char x=a.charAt(j), y=b.charAt(j);
g.computeIfAbsent(x,k->new HashSet<>());
if (g.get(x).add(y)) indeg.put(y, indeg.get(y)+1);
break;
}
}
Queue<Character> q = new ArrayDeque<>();
for (char ch: indeg.keySet()) if (indeg.get(ch)==0) q.offer(ch);
StringBuilder ans = new StringBuilder();
while(!q.isEmpty()){
char ch=q.poll(); ans.append(ch);
for(char nxt: g.getOrDefault(ch, new HashSet<>())) if (indeg.merge(nxt,-1,Integer::sum)==0) q.offer(nxt);
}
return ans.length()==indeg.size()?ans.toString():"";
}
}套路模板 · 火星词典迁移骨架
# 1. 定义状态
state = init()
# 2. 按顺序读输入
for x in data:
update(state, x) # 只做本题允许的安全转移
# 3. 从状态里取答案
return answer(state)// 1. init state
for (auto x : data) {
// update state with the same invariant
}
return ans;// 1. init state
for (int x : data) {
// update state with the same invariant
}
return ans;复杂度
时间复杂度
O(V+E)
V=字母数、E=约束边数;Kahn 拓扑(BFS出入队)每点每边各处理一次,无排序无堆
空间复杂度
O(V+E)
邻接表存图 + 入度数组 + 队列
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 火星词典 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心状态是什么?+
比较相邻单词第一个不同字符,得到有向边;再拓扑排序。
这道题为什么用「高级图」,换最直接的暴力解会差在哪?+
高级图抓住了本题的结构特征,把暴力解里重复的工作省掉;暴力解通常要多嵌套一层枚举,数据一大就超时。具体对比见上文「暴力解及其卡点」与「最优解逐步推演」两节。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。