单词接龙 II 图解题解
这道题到底在问什么
- beginWord
- "hit"
- endWord
- "cog"
- wordList
- [hot,dot,dog,lot,log,cog]
- 输出
- [[hit,hot,dot,dog,cog],[hit,hot,lot,log,cog]]
最优解:一步一步想明白
- 3换个眼光:单词是地图上的点,差一个字母的两词之间有路。问题变成了我们熟悉的图上最短路。
- 4光找到最短长度不够,还要能还原路径。诀窍:BFS 分层时给每个新词记下它的全部前驱(上一层谁能一步变到它)。
- 5下面一帧一帧演:先看 BFS 怎么逐层扩散并记前驱,再看怎么沿前驱回溯拼出全部路径。
- 6queue=[hit],已访问={hit}起跑线:把 hit 入队,记它在第 0 层、已访问。灰边是「差一个字母」的潜在连接,后面 BFS 会逐条点亮真正走通的边。
- 7hit 的邻居 = hot扩展 hit:词表里只有 hot 和它差一个字母(hit→hot)。hot 进入第 1 层,记 parents[hot]={hit}。
- 8parents[hot]={hit}第 0 层处理完(hit 变绿表示已确定)。第 1 层只有 hot,它的前驱已记为 {hit}。这层没有 cog,继续往外扩第 2 层。
- 9hot 邻居 = dot, lot扩展 hot:改首字母能得到 dot(h→d)和 lot(h→l),都在词表。两条岔路同时打开——这正是后面会有两条最短路的根源。记 parents[dot]={hot}、parents[lot]={hot}。
- 10parents[dot]={hot}, parents[lot]={hot}hot 处理完变绿。第 2 层有 dot、lot 两个词,各自记好前驱。仍没碰到 cog,继续扩第 3 层。注意 BFS 同层一起推进,不偏不倚。
- 11dot 邻居 = dog(hot 已访问跳过)扩展 dot:改尾字母得 dog(dog)。hot 已访问、跳过。dog 进第 3 层,记 parents[dog]={dot}。
- 12lot 邻居 = log扩展 lot:改尾字母得 log(log)。log 进第 3 层,记 parents[log]={lot}。两条岔路平行向前,都还没到终点。
- 13parents[dog]={dot}, parents[log]={lot}第 2 层全处理完(dot、lot 变绿)。第 3 层有 dog、log。它们到 cog 各差一个字母,下一层很可能命中——但纪律上仍要把这一层扩完。
- 14dog → cog(d→c)扩展 dog:改首字母得 cog(cog)——终点!cog 进第 4 层,记 parents[cog]={dog}。但别急着停,本层还有 log 没扩。
- 15log → cog,parents[cog] 再加 log扩展 log:也能一步到 cog(log→cog)。cog 已在第 4 层,所以不重复入队,只把 log 追加进 parents[cog]。现在 parents[cog]={dog, log}——两条边都通向终点。
- 16最短长度 4 锁定,parents 记账完毕第 4 层处理完,cog 在本层出现,最短长度锁定为 4 次变换。整张前驱图也建好了:parents 记下了每个词的所有上层来源。下面进入第二阶段——沿前驱回溯拼出所有路径。
- 17path = [cog]回溯从终点 cog 起,倒着往回拼。看前驱表:parents[cog]={dog, log}——有两个来源,意味着待会儿会分裂成两条路。
- 18path = [dog, cog]先走第一条分支:cog 的前驱取 dog,接到路径头部 → [dog, cog]。再查 parents[dog]={dot},只有一个来源,继续往回。
- 19path = [dot, dog, cog]继续:dog 的前驱是 dot → [dot, dog, cog]。查 parents[dot]={hot},再往回接 hot。路径一格格朝起点生长。
- 20path = [hot, dot, dog, cog]dot 的前驱是 hot → [hot, dot, dog, cog]。查 parents[hot]={hit},而 hit 就是起点——再接一步就到头了。
- 21path = [hit, hot, dot, dog, cog]接上起点 hit,触到 beginWord,第一条最短路完工:hit→hot→dot→dog→cog。现在回到 cog 那个分叉口,走另一条分支 log。
- 22path = [log, cog]回到 cog 的分叉口,这次取另一个前驱 log → [log, cog]。查 parents[log]={lot},继续往回拼。
- 23path = [lot, log, cog]log 的前驱是 lot → [lot, log, cog]。查 parents[lot]={hot},殊途同归——又回到了 hot。
- 24path = [hot, lot, log, cog]lot 的前驱是 hot → [hot, lot, log, cog]。再查 parents[hot]={hit},下一步接上起点就收尾。
- 25path = [hit, hot, lot, log, cog]接上 hit,第二条最短路也完工:hit→hot→lot→log→cog。两个分支都回到起点,回溯结束。
- 26两条等长(4 步)最短路整张图看:从 hit 到 cog 共有 两条等长(4 步)的最短路,在 hot 处分叉、在 cog 处汇合。BFS 记前驱 + 回溯,把它们一条不漏地还原了出来。
- 30cog 同时被 dog、log 指向放大看终点 cog:dog 和 log 都能一步变到它。若 parents 只记一个父亲,回溯就只能拼出一条路,另一条凭空消失。所以前驱必须用集合/列表存全部来源。
⚠️ 容易写错的地方
✗ 错:找到 endWord 立刻停,丢了同层其他前驱
✓ 对:本层扫完再停:同层多个词可能都指向 endWord
cog 的前驱有 dog 和 log,提前停会漏掉一条最短路
✗ 错:生成邻居就立刻标 visited 进总表
✓ 对:整层处理完再把本层新词并入 visited
同层两个词可能指向同一新词,过早标记会漏记前驱,丢路径
✗ 错:用普通 BFS 只记一个父亲
✓ 对:parents 存『集合/列表』,允许一个词有多个前驱
最短路不止一条,单父亲只能还原一条
完整代码(Python / C++ / Java)
Python
from collections import deque, defaultdict
def findLadders(beginWord, endWord, wordList):
words = set(wordList)
if endWord not in words: return []
parents = defaultdict(set)
visited = {beginWord}
level = {beginWord}
found = False
while level and not found:
nxt = set()
for w in level:
for i in range(len(w)):
for c in 'abcdefghijklmnopqrstuvwxyz':
t = w[:i] + c + w[i+1:]
if t in words and t not in visited:
if t == endWord: found = True
nxt.add(t)
parents[t].add(w)
visited |= nxt
level = nxt
if not found: return []
res, paths = [], [[endWord]]
while paths and paths[0][0] != beginWord:
np = []
for p in paths:
for pre in parents[p[0]]:
np.append([pre] + p)
paths = np
return pathsC++
class Solution {
public:
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> words(wordList.begin(), wordList.end());
vector<vector<string>> res;
if (!words.count(endWord)) return res;
unordered_map<string, vector<string>> parents;
unordered_set<string> visited{beginWord};
unordered_set<string> level{beginWord};
bool found = false;
while (!level.empty() && !found) {
unordered_set<string> nxt;
for (string w : level) {
for (int i = 0; i < (int)w.size(); ++i) {
char old = w[i];
for (char c = 'a'; c <= 'z'; ++c) {
w[i] = c;
if (words.count(w) && !visited.count(w)) {
if (w == endWord) found = true;
nxt.insert(w);
parents[w].push_back(w.substr(0,i)+old+w.substr(i+1));
}
}
w[i] = old;
}
}
for (const string& s : nxt) visited.insert(s);
level = nxt;
}
if (!found) return res;
vector<vector<string>> paths{{endWord}};
while (!paths.empty() && paths[0][0] != beginWord) {
vector<vector<string>> np;
for (auto& p : paths)
for (string& pre : parents[p[0]]) {
vector<string> q{pre};
q.insert(q.end(), p.begin(), p.end());
np.push_back(q);
}
paths = np;
}
return paths;
}
};Java
class Solution {
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
Set<String> words = new HashSet<>(wordList);
List<List<String>> res = new ArrayList<>();
if (!words.contains(endWord)) return res;
Map<String, List<String>> parents = new HashMap<>();
Set<String> visited = new HashSet<>();
visited.add(beginWord);
Set<String> level = new HashSet<>();
level.add(beginWord);
boolean found = false;
while (!level.isEmpty() && !found) {
Set<String> nxt = new HashSet<>();
for (String w : level) {
char[] arr = w.toCharArray();
for (int i = 0; i < arr.length; i++) {
char old = arr[i];
for (char c = 'a'; c <= 'z'; c++) {
arr[i] = c;
String t = new String(arr);
if (words.contains(t) && !visited.contains(t)) {
if (t.equals(endWord)) found = true;
nxt.add(t);
parents.computeIfAbsent(t, k -> new ArrayList<>()).add(w);
}
}
arr[i] = old;
}
}
visited.addAll(nxt);
level = nxt;
}
if (!found) return res;
List<List<String>> paths = new ArrayList<>();
List<String> init = new ArrayList<>(); init.add(endWord);
paths.add(init);
while (!paths.isEmpty() && !paths.get(0).get(0).equals(beginWord)) {
List<List<String>> np = new ArrayList<>();
for (List<String> p : paths)
for (String pre : parents.getOrDefault(p.get(0), new ArrayList<>())) {
List<String> q = new ArrayList<>(); q.add(pre); q.addAll(p);
np.add(q);
}
paths = np;
}
return paths;
}
}复杂度
时间复杂度
O(N·L²·26)
N 个词,每词长 L,每位试 26 个字母、每次造词 O(L);回溯阶段与路径总数成正比
空间复杂度
O(N·L)
visited、parents 前驱图、各层集合;最坏路径数可能很多
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 单词接龙 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和 LC127(只求最短长度)有什么区别?+
127 只要长度,BFS 第一次到终点的层数即可,无需记前驱;126 要还原所有路径,必须记 parents 并回溯,代价大得多。
怎么进一步加速?+
用双向 BFS:从 beginWord 和 endWord 两头同时分层扩,哪边当前层小扩哪边,两头相遇时拼接前驱。能把搜索宽度大幅压低,是本题的标准优化。
邻居怎么找更快?+
可预处理通配模式:把每个词的每位换成 '*' 当桶 key(如 h*t),同桶的词互为邻居,免去逐位试 26 个字母,把建图均摊到 O(N·L)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 单词接龙 II 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。