← 2026-08-21 📂 All Days 2026-08-23 →

🔬 Saturday Deep Dive: Word Ladder — 双向 BFS 最短变换路径 (15 min read)

📊 NeetCode: 91/150 · SysDesign: 60/60 · Behavioral: 60/60 · Frontend: 37/50 · AI: 30/30

🔥 117-day streak!

🔗 LeetCode: https://leetcode.com/problems/word-ladder/

📹 NeetCode: https://neetcode.io/problems/word-ladder


Overview / 概述

中文: Word Ladder 是图遍历大章节的 Boss 关卡。给定起始词、目标词和词典,每次只能改变一个字母,且每个中间状态必须在词典中,问从起始词变换到目标词的最短步数。

表面上看是字符串题,但本质是一道最短路径题。每个单词是图中的节点,两个仅差一个字母的单词之间存在一条无向边。目标:从 beginWord 到 endWord 的最短边数。

这道题是 BFS 思维的终极考题,也是面试中双向 BFS(bidirectional BFS)最经典的应用场景。面试官出这道题,考的不只是"会 BFS",而是你能不能主动提出优化并实现它。

English: Word Ladder is the Boss fight of the graph traversal chapter. Given a start word, end word, and dictionary, you can change exactly one letter per step — each intermediate word must be in the dictionary. Find the shortest transformation sequence length.

It looks like a string problem, but it's fundamentally a shortest path problem. Each word is a graph node; two words that differ by one letter share an undirected edge. Goal: shortest edge count from beginWord to endWord.

This is the ultimate BFS thinking test. The interview question isn't just "can you BFS?" — it's "can you proactively optimize to bidirectional BFS and implement it cleanly?"

Problem: Given beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"], return 5.

Path: `hit → hot → dot → dog → cog` (5 words = 4 transformations, return length = 5)

Part 1: Theory / 理论基础 (5 min)

为什么是 BFS?/ Why BFS?

中文: BFS 天然计算无权图的最短路径。它按层展开——第 1 层是所有距离为 1 的节点,第 2 层是所有距离为 2 的节点……一旦第一次到达终点,当前层数就是最短路径长度。DFS 不行:DFS 会探索更长的路径,无法保证第一次到达即最短。

English: BFS naturally computes shortest paths in unweighted graphs. It expands level by level — layer 1 = all nodes at distance 1, layer 2 = distance 2 … The moment you first reach the destination, the current layer is the shortest path. DFS won't work: it explores longer paths and can't guarantee first-arrival = shortest.

图建模 / Graph Model

每个单词 = 节点 (node)
两词差一字母 = 无向边 (undirected edge)
边权 = 1 (unweighted)

"hit" ——— "hot" ——— "dot" ——— "dog" ——— "cog"
                     |                    |
                    "lot" ——— "log" ———

朴素做法:每个单词对之间判断是否差一字母,构建邻接表。O(N² × L),N 是词典大小,L 是单词长度。

双向 BFS 的直觉 / Bidirectional BFS Intuition

中文: 普通 BFS 从 beginWord 向外扩展。如果图的"分支因子"(branching factor)是 B,深度是 d,则搜索空间是 O(B^d)。

双向 BFS 的思路:同时从 beginWord 和 endWord 出发向中间扩展,两边相遇即停止。每边只搜索深度 d/2,总搜索空间 = O(2 × B^(d/2)) = O(B^(d/2))。

当 d 较大时,这是指数级的加速。

English: Regular BFS from beginWord explores O(B^d) nodes where B = branching factor, d = depth. Bidirectional BFS meets in the middle — each side explores depth d/2, giving O(2 × B^(d/2)) = O(B^(d/2)) nodes. Exponential speedup as d grows.

普通 BFS: 双向 BFS: beginWord beginWord endWord ↓ ↓ ↓ layer 1 layer 1 layer 1 ↓ ↓ ↓ layer 2 layer 2 layer 2 ↓ ↓ ↓ layer 3 layer 3 ←→ layer 3 (相遇!) ↓ layer 4 (endWord) 搜索 B^4 个节点 搜索 2×B^2 个节点 (B=2时: 16 vs 8)

虚拟节点技巧 / Virtual Node Trick

中文: 判断两单词是否差一字母是 O(L²) 的操作(N 个词两两比较)。更快的方法:用通配符模式作为虚拟节点。

"hit" 对应三个虚拟节点:"it", "ht", "hi"。所有匹配同一模式的单词(如 "hit" 和 "hot" 都匹配 "ht")通过该虚拟节点相连。这将邻接表构建从 O(N²L) 降至 O(NL)。

English: Checking every word pair for one-letter difference is O(N²L). The virtual node trick: use wildcard patterns as intermediate nodes. "hit" maps to patterns "it", "ht", "hi". All words matching the same pattern (e.g., "hit" and "hot" both match "ht") are connected through that virtual node. Reduces adjacency construction from O(N²L) to O(NL).


Part 2: Step-by-Step Implementation / 一步一步实现 (8 min)

方法一:朴素 BFS / Approach 1: Basic BFS

时间复杂度 Time: O(N × L × 26) ≈ O(NL)

空间复杂度 Space: O(N × L)

from collections import deque

def ladderLength_basic(beginWord: str, endWord: str, wordList: list[str]) -> int:
    # Convert to set for O(1) lookup
    word_set = set(wordList)

    # endWord must be in dictionary
    if endWord not in word_set:
        return 0

    # BFS queue: (current_word, current_steps)
    # Start with beginWord, step count = 1 (beginWord itself counts)
    queue = deque([(beginWord, 1)])

    # Visited set: don't revisit words (avoids cycles)
    visited = {beginWord}

    while queue:
        word, steps = queue.popleft()

        # Try changing each position
        for i in range(len(word)):
            # Try all 26 possible letters
            for c in 'abcdefghijklmnopqrstuvwxyz':
                # Build the candidate by replacing position i
                candidate = word[:i] + c + word[i+1:]

                # Skip if same word or already visited
                if candidate == word or candidate in visited:
                    continue

                # Found endWord — this is the shortest path
                if candidate == endWord:
                    return steps + 1

                # Add to queue if in dictionary
                if candidate in word_set:
                    visited.add(candidate)
                    queue.append((candidate, steps + 1))

    # No path found
    return 0

关键洞察 / Key insight: We don't need to pre-build a graph. Generate neighbors on-the-fly by trying all 26-letter substitutions at each position. The word_set gives O(1) validity check.


方法二:双向 BFS / Approach 2: Bidirectional BFS ⭐ (面试亮点)

时间复杂度 Time: O(NL × 26) but with ~√ factor speedup in practice

空间复杂度 Space: O(N × L)

from collections import deque

def ladderLength(beginWord: str, endWord: str, wordList: list[str]) -> int:
    word_set = set(wordList)

    if endWord not in word_set:
        return 0

    # Two frontiers: one starting from beginWord, one from endWord
    # Each frontier is a SET (not queue) — we expand the SMALLER one each step
    front = {beginWord}   # frontier expanding from begin
    back  = {endWord}     # frontier expanding from end

    # Total steps taken (each iteration = 1 step from ONE side)
    steps = 1

    while front and back:
        # ALWAYS expand the smaller frontier — this is the key optimization
        # If we always pick the smaller side, we keep both sides balanced
        if len(front) > len(back):
            front, back = back, front

        # Generate next layer for the current frontier
        next_front = set()

        for word in front:
            for i in range(len(word)):
                for c in 'abcdefghijklmnopqrstuvwxyz':
                    candidate = word[:i] + c + word[i+1:]

                    # If this candidate appears in the OTHER frontier → MEET!
                    # We found a path: steps from front side + steps from back side
                    if candidate in back:
                        return steps + 1

                    # Only expand through valid dictionary words
                    if candidate in word_set:
                        next_front.add(candidate)

        # Move to next layer; remove visited words from word_set
        # (prevents re-visiting from the same side or infinite loops)
        word_set -= next_front
        front = next_front
        steps += 1

    # No path
    return 0

架构图 / Architecture Diagram:

Step 1: front={hit} back={cog} steps=1 ↓ expand front (size 1) next={hot} hot ∈ back? No Step 2: front={hot} back={cog} steps=2 ↓ expand front (size 1) next={dot, lot} dot ∈ back? No; lot ∈ back? No Step 3: front={dot,lot} back={cog} steps=3 back is smaller (size 1) → expand back! ↓ expand back next_back={dog, log} dog ∈ front? No; log ∈ front? No Step 4: front={dot,lot} back={dog,log} steps=4 fronts are equal size, expand front next_front from dot: {dog,lot,...} dog ∈ back? YES → return steps+1 = 5 ✅

Cross-check (Step 4 verification):

Input: beginWord="hit", endWord="cog", wordList=["hot","dot","dog","lot","log","cog"]

Tracing through the diagram vs code:

  • Step 1: front={"hit"}, try all substitutions of "hit": h→a-z at pos 0 = "ait","bit"... none in dict; pos 1 = "hat","hbt"... none in dict; pos 2 = "hia","hib"... "hot" is in word_set ✓. next_front={"hot"}. "hot" in back={"cog"}? No. front={"hot"}, steps=2.
  • Step 2: expand front={"hot"} (size 1 ≤ back size 1, same): from "hot": "dot","lot" in word_set. Neither in back. front={"dot","lot"}, steps=3.
  • Step 3: len(front)=2 > len(back)=1 → swap! Now expanding back={"cog"}: "aog"..."log","dog" in word_set. Neither in swapped front={"dot","lot"}. back={"dog","log"}, steps=4.
  • Step 4: len(front)=2 == len(back)=2 → expand front. From "dot": pos 2 change → "dog" in word_set → "dog" in back={"dog","log"}? YES! Return steps+1 = 5. ✅

Diagram matches code exactly.


方法三:虚拟节点 BFS / Approach 3: Virtual Node BFS

适合在面试中提及,理解其思路 / Good to mention in interviews as an insight:

from collections import defaultdict, deque

def ladderLength_virtual(beginWord: str, endWord: str, wordList: list[str]) -> int:
    # Build adjacency: pattern → list of words matching that pattern
    # "hit" → {"*it": ["hit",...], "h*t": ["hit","hot",...], "hi*": ["hit",...]}
    graph = defaultdict(list)

    for word in wordList + [beginWord]:
        for i in range(len(word)):
            # Wildcard pattern for position i
            pattern = word[:i] + '*' + word[i+1:]
            graph[pattern].append(word)

    # BFS on virtual graph: real words AND patterns as nodes
    queue = deque([(beginWord, 1)])
    visited = {beginWord}

    while queue:
        node, steps = queue.popleft()

        # Generate all patterns this word matches
        for i in range(len(node)):
            pattern = node[:i] + '*' + node[i+1:]

            # All words sharing this pattern are neighbors
            for neighbor in graph[pattern]:
                if neighbor == endWord:
                    return steps + 1
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append((neighbor, steps + 1))

    return 0

复杂度对比 / Complexity Comparison

方法 / Method时间 Time空间 Space面试推荐
朴素 BFSO(N × L × 26)O(N × L)起点,先写这个
双向 BFSO(N × L × 26 / √)O(N × L)⭐ 主动提出
虚拟节点 BFSO(N × L)O(N × L²)作为补充提及

N = 词典大小, L = 单词长度


Part 3: Edge Cases & Gotchas / 边界情况 (2 min)

中文常见坑 / Common pitfalls:

1. endWord 不在词典中 / endWord not in wordList

if endWord not in word_set:
    return 0  # 别忘了这个检查!

这是最常见的 bug。beginWord 可以不在词典中(题目说明),但 endWord 必须在。

2. beginWord == endWord

# 题目保证 beginWord != endWord,但防御性编程:
if beginWord == endWord:
    return 1

3. 双向 BFS 中忘记交换 / Forgetting to swap in bidirectional BFS

# Wrong: always expand `front`
# Right:
if len(front) > len(back):
    front, back = back, front

如果忘记交换,退化成单向 BFS,失去所有优化优势。

4. 双向 BFS 的返回值 / Return value in bidirectional BFS

Meeting at step N means: we've taken N steps from the start side and are finding a match in the other frontier. The other frontier was already extended to cover those words. Return steps + 1.

  • steps = number of layers expanded so far (from whichever side expanded each round)
  • When we find a meeting point, we add 1 to account for the final step connecting the two frontiers.

5. 修改 word_set 防止重复访问 / Mutating word_set to prevent revisits

word_set -= next_front  # Remove explored words from the global set

如果不这样做,已经访问过的词可能从另一侧再次入队,导致死循环或错误结果。

6. beginWord 可能也在 wordList 中

题目不保证。用 word_set -= {beginWord} 防止重复访问自身。


Part 4: Real-World Application / 实际应用 (2 min)

中文: Word Ladder 的算法思想在生产系统中无处不在:

1. 拼写纠错 (Spell Correction)

Google/iOS 键盘的拼写建议:给定一个错误单词,找到编辑距离最近的正确单词。BFS 按编辑距离层次扩展,等价于 Word Ladder 的变体。

2. 知识图谱推理 (Knowledge Graph Reasoning)

从实体 A 到实体 B 的最短关系路径。例如 WikiData:"北京" → ??? → "苹果公司"。每条边是一种关系类型,问题等价于最短 hop 路径。

3. 配置迁移 (Configuration Migration)

系统配置从版本 v1 迁移到 v5,每步只能应用一个合法的 migration 脚本,问最少几步?等价于图最短路径。

4. DNA 序列变异分析

生物信息学:从基因序列 A 到序列 B,每步只能改变一个碱基对(且中间序列必须是有意义的序列)。

English: Real production uses of this pattern:

  • Autocorrect / fuzzy matching — find shortest edit distance to valid word; BFS on "word space" graph
  • Knowledge graph shortest path — "6 degrees of separation" style queries in Neo4j / WikiData
  • Network routing — each hop changes one attribute (VLAN, subnet) of a packet's path
  • Code refactoring chains — automated refactoring where each intermediate state must compile

The bidirectional BFS pattern specifically appears in:

  • LinkedIn's "degrees of connection" (bidi BFS between two users' connection graphs)
  • Route planning (meet-in-the-middle optimization in GPS routing, e.g. A* bidirectional)
  • Password cracking chains (rainbow tables = precomputed hash chain meet-in-the-middle)

Part 5: Interview Simulation / 面试模拟 (3 min)

Q1: 为什么用 BFS 而不是 DFS?Why BFS over DFS?

A: BFS guarantees shortest path in unweighted graphs by expanding layer by layer — the first time we reach a node, we've found the minimum distance. DFS might find a valid path but not the shortest one. Since the problem asks for minimum transformation length, BFS is the correct choice.


Q2: 你能优化你的初始 BFS 解法吗?Can you optimize your basic BFS?

A: Yes — bidirectional BFS. Instead of expanding from just one end, we simultaneously expand from both beginWord and endWord. We always expand the smaller frontier to keep both sides balanced. This reduces the search space from O(B^d) to O(B^(d/2)), an exponential improvement. In the worst case with branching factor 26 and depth 10, that's 26^10 vs 2×26^5 — roughly 11 billion nodes vs 22 million.


Q3: 双向 BFS 怎么知道两侧相遇了?How do you detect meeting in bidirectional BFS?

A: When expanding one frontier, check if any generated candidate appears in the OTHER frontier's set. If yes, the two sides have met — return steps + 1. Using sets (not queues) for frontiers enables O(1) lookup for this check.


Q4: 如果 beginWord 也需要在词典中怎么办?What if beginWord must also be in wordList?

A: The current LeetCode problem says beginWord doesn't need to be in wordList — it's the given start. If the constraint changed, we'd simply check if beginWord not in word_set: return 0 at the top, similar to the endWord check. The rest of the algorithm stays identical.


Q5: 这道题的 Follow-up:输出完整路径怎么做?Print the actual path?

A: That's LeetCode 126 (Word Ladder II — Hard). For a single shortest path, augment BFS to track parent pointers: parent[candidate] = word. After reaching endWord, backtrack from endWord to beginWord using the parent map. For ALL shortest paths (LC 126), we must be careful: allow multiple parents per node at the same BFS layer, and backtrack through all of them — this is significantly more complex and requires careful same-level handling.


总结 / Summary

Word Ladder 解题模板: 1. 特判: endWord 不在词典 → return 0 2. 初始化: front={beginWord}, back={endWord}, steps=1 3. 循环: a. 选小的前沿扩展 (if len(front) > len(back): swap) b. 枚举每个词的所有26字母替换 c. 如果 candidate ∈ 对面前沿 → return steps+1 d. 如果 candidate ∈ word_set → 加入 next_front e. word_set -= next_front; front = next_front; steps++ 4. 循环结束无相遇 → return 0 关键提示: - BFS = 无权图最短路径 - 双向 BFS = √ 级别加速 - 永远扩展较小的前沿 - word_set -= next_front 防止重复访问

时间复杂度 / Time: O(N × L × 26) where N = wordList size, L = word length

空间复杂度 / Space: O(N × L)