Find the shortest sequence of single-letter changes that transforms beginWord into endWord, using only words from a dictionary. The trick is treating each word as a graph node and running BFS — shortest path by construction — with wildcard-bucket adjacency to find neighbors in O(L) instead of O(N·L).
Given beginWord, endWord, and a wordList, return the number of words in the shortest transformation sequence where each adjacent pair differs by exactly one letter and every intermediate word is in the list. If no such sequence exists, return 0.
Concrete example: beginWord="hit", endWord="cog", wordList=["hot","dot","dog","lot","log","cog"].
The shortest path is hit → hot → dot → dog → cog (5 words), so the answer is 5.
endWord. BFS explores layer by layer, so the first time it touches the target is guaranteed to be the shortest path. The only hard part is finding neighbors efficiently — that's where wildcard buckets come in.endWord is not in wordList, return 0immediately — we can never reach it.i, create the key word[0..i-1] + "*" + word[i+1..] and map it to all words that share that pattern. Example: "h*t" → ["hit", "hot"]. This lets us find all neighbors of a word in O(L) bucket lookups rather than scanning all N words.queue = [beginWord], depth = 1. Process one full BFS level per outer loop iteration (level-order BFS). For each word, try all L wildcard patterns, look up their buckets, and enqueue any unvisited neighbors.endWord as a neighbor, return the current depth + 1. BFS guarantees this is the minimum.endWord, return 0.beginWord and endWord), not the number of transformation steps. So initialize depth = 1 (counting beginWord) and increment at each BFS level. Many implementations start at 0 and get an off-by-one answer.For very large word lists, bidirectional BFS expands from both beginWord and endWord simultaneously, meeting in the middle. This reduces the search space from O(b^d) to O(b^(d/2)) where b = branching factor and d = depth. Same O(N·L²) worst-case preprocessing, but dramatically fewer nodes visited in practice.
wordSet from wordList. Early-exit if cog is not present. Seed queue with hit, mark it visited, start depth = 1.1function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {2▶ const wordSet = new Set<string>(wordList);3▶ if (!wordSet.has(endWord)) return 0;45 // Build wildcard buckets: "h*t" -> ["hit","hot"]6 const buckets = new Map<string, string[]>();7 for (const word of [beginWord, ...wordList]) {8 for (let i = 0; i < word.length; i++) {9 const key = word.slice(0, i) + '*' + word.slice(i + 1);10 const list = buckets.get(key) ?? [];11 list.push(word);12 buckets.set(key, list);13 }14 }1516▶ const visited = new Set<string>([beginWord]);17▶ const queue: string[] = [beginWord];18▶ let depth = 1;1920 while (queue.length > 0) {21 depth++;22 const size = queue.length;23 for (let qi = 0; qi < size; qi++) {24 const word = queue.shift()!;25 for (let i = 0; i < word.length; i++) {26 const pattern = word.slice(0, i) + '*' + word.slice(i + 1);27 for (const neighbor of buckets.get(pattern) ?? []) {28 if (neighbor === endWord) return depth;29 if (!visited.has(neighbor)) {30 visited.add(neighbor);31 queue.push(neighbor);32 }33 }34 }35 }36 }3738 return 0; // endWord unreachable39}
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const wordSet = new Set<string>(wordList);
if (!wordSet.has(endWord)) return 0;
// Build wildcard buckets: "h*t" -> ["hit","hot"]
const buckets = new Map<string, string[]>();
for (const word of [beginWord, ...wordList]) {
for (let i = 0; i < word.length; i++) {
const key = word.slice(0, i) + '*' + word.slice(i + 1);
const list = buckets.get(key) ?? [];
list.push(word);
buckets.set(key, list);
}
}
const visited = new Set<string>([beginWord]);
const queue: string[] = [beginWord];
let depth = 1;
while (queue.length > 0) {
depth++;
const size = queue.length;
for (let qi = 0; qi < size; qi++) {
const word = queue.shift()!;
for (let i = 0; i < word.length; i++) {
const pattern = word.slice(0, i) + '*' + word.slice(i + 1);
for (const neighbor of buckets.get(pattern) ?? []) {
if (neighbor === endWord) return depth;
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
}
}
return 0; // endWord unreachable
}wordList to a Set<string> for O(1) membership checks, then immediately return 0 if endWordisn't present — no sequence can exist.beginWord) and each index i, create the key word[0..i-1] + "*" + word[i+1..]. Two words share a bucket entry iff they match at every position except i — exactly the one-letter-different criterion. This O(N·L²) preprocessing makes each BFS expansion O(L) instead of O(N·L).beginWord and mark it visited immediately (not when dequeued) to prevent duplicate enqueues. Start depth = 1 counting beginWord itself.while runs one BFS level per iteration. size = queue.length snapshots the current level boundary before we start adding the next one. Incrementing depth at the top of each outer loop counts words, not edges.L wildcard patterns. Looking up each bucket surfaces exactly the words reachable in one letter change. If a neighbor is endWord, return depth — BFS guarantees this is the shortest path. Otherwise, enqueue unvisited neighbors.endWord, the word graph has no path from beginWord to endWord.L → O(N·L²) total. BFS visits each word at most once and tries O(L) patterns per word, with O(1) amortized bucket lookups → O(N·L) for BFS. Space: O(N·L²) for the bucket map.endWord, then DFS-reconstruct all paths from the parent map.beginWord, one from endWord. Each iteration, expand the smaller frontier. When they meet, sum both depths. This is O(b^(d/2)) vs O(b^d) in practice.1 (the sequence is just the single word). Guard this before starting BFS.function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const wordSet = new Set<string>(wordList);
if (!wordSet.has(endWord)) return 0;
// Build wildcard buckets: "h*t" -> ["hit","hot"]
const buckets = new Map<string, string[]>();
for (const word of [beginWord, ...wordList]) {
for (let i = 0; i < word.length; i++) {
const key = word.slice(0, i) + '*' + word.slice(i + 1);
const list = buckets.get(key) ?? [];
list.push(word);
buckets.set(key, list);
}
}
const visited = new Set<string>([beginWord]);
const queue: string[] = [beginWord];
let depth = 1;
while (queue.length > 0) {
depth++;
const size = queue.length;
for (let qi = 0; qi < size; qi++) {
const word = queue.shift()!;
for (let i = 0; i < word.length; i++) {
const pattern = word.slice(0, i) + '*' + word.slice(i + 1);
for (const neighbor of buckets.get(pattern) ?? []) {
if (neighbor === endWord) return depth;
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
}
}
return 0; // endWord unreachable
}L → O(N·L²) total. BFS visits each word at most once and tries O(L) patterns per word, with O(1) amortized bucket lookups → O(N·L) for BFS. Space: O(N·L²) for the bucket map.Search from both beginWord and endWord at once, always expanding the smaller frontier and stopping the instant the two waves collide — far fewer nodes touched in practice.
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const wordSet = new Set<string>(wordList);
if (!wordSet.has(endWord)) return 0;
// Wildcard buckets: "h*t" -> ["hit","hot"], so neighbors cost O(L) to find.
const buckets = new Map<string, string[]>();
for (const word of [beginWord, ...wordList]) {
for (let i = 0; i < word.length; i++) {
const key = word.slice(0, i) + '*' + word.slice(i + 1);
const list = buckets.get(key) ?? [];
list.push(word);
buckets.set(key, list);
}
}
// Two frontiers, one growing from each end. 'seen' maps a word to which side
// reached it, so a collision = a word already claimed by the OTHER side.
let front = new Set<string>([beginWord]);
let back = new Set<string>([endWord]);
const seen = new Set<string>([beginWord, endWord]);
let depth = 1;
while (front.size > 0 && back.size > 0) {
// Always expand the smaller frontier to keep the branching factor low.
if (front.size > back.size) {
const tmp = front;
front = back;
back = tmp;
}
depth++;
const next = new Set<string>();
for (const word of front) {
for (let i = 0; i < word.length; i++) {
const pattern = word.slice(0, i) + '*' + word.slice(i + 1);
for (const neighbor of buckets.get(pattern) ?? []) {
// If the other wave already owns this word, the paths meet here.
if (back.has(neighbor)) return depth;
if (!seen.has(neighbor)) {
seen.add(neighbor);
next.add(neighbor);
}
}
}
}
front = next;
}
return 0; // frontiers never met -> unreachable
}endWord ∈ wordList — otherwise the back frontier can never touch a real word, handled by the early return.Skip the bucket map entirely: at each position swap in every letter a–z and keep the candidates that exist in the word set.
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const wordSet = new Set<string>(wordList);
if (!wordSet.has(endWord)) return 0;
const visited = new Set<string>([beginWord]);
let queue: string[] = [beginWord];
let depth = 1;
while (queue.length > 0) {
depth++;
const next: string[] = [];
for (const word of queue) {
// For each position, try substituting all 26 lowercase letters.
for (let i = 0; i < word.length; i++) {
for (let c = 97; c < 123; c++) {
const ch = String.fromCharCode(c);
if (ch === word[i]) continue; // same word, skip
const candidate = word.slice(0, i) + ch + word.slice(i + 1);
if (candidate === endWord) return depth;
// Only follow candidates that are real dictionary words.
if (wordSet.has(candidate) && !visited.has(candidate)) {
visited.add(candidate);
next.push(candidate);
}
}
}
}
queue = next;
}
return 0; // endWord unreachable
}L is small; it loses to the bucket map only because it probes letters that produce non-words.Skip the wildcard buckets and the 26-letter trick: compare every pair of words, link the two whenever they differ by exactly one letter, then run an ordinary BFS over that explicit adjacency list.
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const words = [beginWord, ...wordList];
if (!wordList.includes(endWord)) return 0;
// Do two equal-length words differ in exactly one position?
function oneApart(a: string, b: string): boolean {
let diff = 0;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i] && ++diff > 1) return false;
}
return diff === 1;
}
// Adjacency list built from all O(N²) pairwise comparisons.
const adj = new Map<string, string[]>();
for (const w of words) adj.set(w, []);
for (let i = 0; i < words.length; i++) {
for (let j = i + 1; j < words.length; j++) {
if (oneApart(words[i], words[j])) {
adj.get(words[i])!.push(words[j]);
adj.get(words[j])!.push(words[i]);
}
}
}
const visited = new Set<string>([beginWord]);
let queue: string[] = [beginWord];
let depth = 1;
while (queue.length > 0) {
const next: string[] = [];
for (const word of queue) {
if (word === endWord) return depth;
for (const nb of adj.get(word) ?? []) {
if (!visited.has(nb)) {
visited.add(nb);
next.push(nb);
}
}
}
queue = next;
depth++;
}
return 0; // endWord unreachable
}N² word pairs at O(L) each — O(N² · L) and quadratic memory. The wildcard-bucket version discovers neighbors in O(L) per word without ever materializing the full pairwise graph.| "shortest transformation sequence" | BFS on word graph |
| words differing by one character → neighbors | wildcard-bucket adjacency map |
| shortest path on an unweighted implicit graph | level-order BFS with visited set |
| "all shortest paths" variant | BFS + parent map + DFS reconstruction |
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const wordSet = new Set<string>(wordList);
if (!wordSet.has(endWord)) return 0;
// build wildcard buckets
const buckets = new Map<string, string[]>();
for (const word of [beginWord, ...wordList]) {
for (let i = 0; i < word.length; i++) {
const key = word.slice(0, i) + '*' + word.slice(i + 1);
// bucket[key].push(word)
}
}
const visited = new Set<string>([beginWord]);
const queue: string[] = [beginWord];
let depth = 1;
while (queue.length > 0) {
depth++;
const size = queue.length;
for (let qi = 0; qi < size; qi++) {
const word = queue.shift()!;
// try every wildcard pattern, look up bucket, enqueue unvisited neighbors
// if neighbor === endWord return depth
}
}
return 0;
}beginWord="hit", endWord="cog", wordList=["hot","dot","dog","lot","log","cog"], what does ladderLength return?beginWord="hot", endWord="dog", wordList=["hot","dot","dog"], what is returned?