Hard#1278 min readGraphsBFSShortest PathHashingLeetCode

Word Ladder

BFS on an implicit word graph, with wildcard buckets

BFS on an implicit graph. Each word is a node, edges connect words differing by one letter.

Published Apr 16, 2026

Problem

Given two words beginWord and endWord and a wordList, return the length of the shortest transformation sequence from beginWord to endWord such that:

  1. Each transformed word differs from the previous by exactly one letter.
  2. Each intermediate word exists in wordList.
  3. Return 0 if no sequence exists.

The length counts words in the sequence (including both endpoints).

Input
beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
Output
5
Explanation
hit → hot → dot → dog → cog (5 words).
Input
beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
Output
0
Explanation
"cog" is not in the list, so no valid final step.

Intuition

This is the shortest-path problem on a graph where nodes are words and edges connect words that differ by exactly one letter. We want the minimum number of edges + 1.

BFS from beginWord solves shortest-path in unweighted graphs in O(V + E). The tricky part is computing the edges efficiently. The naive approach — for every pair of words, count differing characters — is O( · L) and timeouts on big inputs.

The wildcard bucket trick cuts this to O(N · ) edges: for each word in the list, generate L patterns by replacing one character at a time with * (e.g. "hot""*ot", "h*t", "ho*"). Words that share any pattern differ by exactly one letter. The bucket pattern [words] becomes the adjacency representation we need.

Bidirectional BFS further halves the depth: run BFS from both ends, alternating the smaller frontier, and stop when they meet. Reduces worst-case cost from b^d to 2 · b^(d/2) where b is branching factor.

Interactive concept

Word Ladder BFS Frontier

Shortest transformation length is a BFS distance in an implicit word graph.

Words are graph nodes

Edges connect words that differ by one character; the full graph is implicit.

hit
hot
dot
dog
lot
log
cog

Approach

BFS with wildcard buckets

TimeO(N·L²)
SpaceO(N·L²)
Recommended
  1. Build a map pattern [words] where each pattern has one wildcard.
  2. Standard BFS from beginWord. For each current word, generate its L patterns; each pattern buckets points to N words. Push unvisited ones.
  3. Stop when endWord is popped; return the level count.
use std::collections::{HashMap, HashSet, VecDeque};

impl Solution {
    pub fn ladder_length(
        begin_word: String,
        end_word: String,
        word_list: Vec<String>,
    ) -> i32 {
        let word_set: HashSet<String> = word_list.iter().cloned().collect();
        if !word_set.contains(&end_word) {
            return 0;
        }
        let l = begin_word.len();
        let mut buckets: HashMap<String, Vec<String>> = HashMap::new();
        for w in word_set.iter() {
            let bytes = w.as_bytes();
            for i in 0..l {
                let mut key: Vec<u8> = bytes.to_vec();
                key[i] = b'*';
                buckets
                    .entry(String::from_utf8(key).unwrap())
                    .or_insert_with(Vec::new)
                    .push(w.clone());
            }
        }
        let mut visited: HashSet<String> = HashSet::new();
        let mut q: VecDeque<(String, i32)> = VecDeque::new();
        q.push_back((begin_word.clone(), 1));
        visited.insert(begin_word);
        while let Some((w, steps)) = q.pop_front() {
            if w == end_word {
                return steps;
            }
            let bytes = w.as_bytes();
            for i in 0..l {
                let mut key: Vec<u8> = bytes.to_vec();
                key[i] = b'*';
                let k = String::from_utf8(key).unwrap();
                if let Some(list) = buckets.get(&k) {
                    for nb in list {
                        if !visited.contains(nb) {
                            visited.insert(nb.clone());
                            q.push_back((nb.clone(), steps + 1));
                        }
                    }
                }
            }
        }
        0
    }
}
Rust · runnable

Bidirectional BFS

TimeO(N·L²)
SpaceO(N·L²)

Maintain two frontiers — one from beginWord, one from endWord. Always expand the smaller one; if a newly visited word is already in the other frontier, we've met in the middle. The total depth to meet is halved.

This usually wins on dense word lists with long solutions. On tiny inputs the overhead of two frontiers makes it slower; benchmark if it matters.

use std::collections::{HashMap, HashSet};

impl Solution {
    pub fn ladder_length(
        begin_word: String,
        end_word: String,
        word_list: Vec<String>,
    ) -> i32 {
        let words: HashSet<String> = word_list.iter().cloned().collect();
        if !words.contains(&end_word) {
            return 0;
        }
        let l = begin_word.len();
        let mut buckets: HashMap<String, Vec<String>> = HashMap::new();
        for w in words.iter() {
            let bytes = w.as_bytes();
            for i in 0..l {
                let mut key: Vec<u8> = bytes.to_vec();
                key[i] = b'*';
                buckets
                    .entry(String::from_utf8(key).unwrap())
                    .or_insert_with(Vec::new)
                    .push(w.clone());
            }
        }
        let mut front: HashSet<String> = HashSet::from([begin_word.clone()]);
        let mut back: HashSet<String> = HashSet::from([end_word.clone()]);
        let mut seen: HashSet<String> = HashSet::from([begin_word, end_word]);
        let mut steps = 1;
        while !front.is_empty() && !back.is_empty() {
            if front.len() > back.len() {
                std::mem::swap(&mut front, &mut back);
            }
            let mut next: HashSet<String> = HashSet::new();
            for w in front.iter() {
                let bytes = w.as_bytes();
                for i in 0..l {
                    let mut key: Vec<u8> = bytes.to_vec();
                    key[i] = b'*';
                    let k = String::from_utf8(key).unwrap();
                    if let Some(list) = buckets.get(&k) {
                        for nb in list {
                            if back.contains(nb) {
                                return steps + 1;
                            }
                            if seen.insert(nb.clone()) {
                                next.insert(nb.clone());
                            }
                        }
                    }
                }
            }
            front = next;
            steps += 1;
        }
        0
    }
}
Rust · runnable

Trace

Single-ended BFS on hit cog with list ["hot","dot","dog","lot","log","cog"]:

BFS layers#127

Word Ladder walkthrough

hit → cog, words = [hot,dot,dog,lot,log,cog]

Step 1 of 30%
hit
hot
dot
dog
lot
log
cog
hit-hot
level
1 → 2
visited
2

hit maps to *it, h*t, and hi*.

Generate wildcard buckets

Decision

Only h*t has a useful neighbor: hot.

Update

Queue hot at level 2 and mark it visited.

Why it works

One-letter changes correspond to sharing one wildcard bucket.

Invariant

BFS levels are transformation counts; the first time we pop the target is the shortest chain.

Finish

cog is reached at level 5.

Mark words visited when enqueued, not when popped.Wildcard buckets prevent O(N^2) pairwise word comparisons.

Five words in the shortest chain.

Edge cases

  • endWord not in list — return 0 immediately; avoids useless BFS.
  • beginWord == endWord — problem constraints usually forbid this; if you hit it, returning 0 or 1 depends on interpretation (LeetCode excludes it).
  • No path exists — BFS exhausts without meeting; return 0.
  • Long words, small list — wildcard buckets are still O(N · L); the factor is per BFS, not per bucket build.

Takeaway

When the graph is implicit, the first question is "how do I generate edges without blowing up time?" The wildcard bucket trick is a pattern that shows up whenever neighbors are defined by "one step away" — single-letter change, single-digit change, single-bit flip. Precompute the bucketed adjacency once, then run a standard BFS on top.

More in Graphs: BFS & DFS