word transformation
algorithms
computational linguistics
word ladder
graph theory

Algorithm to transform one word to another through valid words

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

This problem is usually known as the Word Ladder problem: change one word into another by altering one letter at a time, while every intermediate word must remain valid. It looks like a language puzzle, but algorithmically it is a shortest-path problem on an implicit graph.

The right mental model is simple. Each word is a node, and two words are connected when they differ by exactly one character. Once you see that graph, breadth-first search becomes the standard solution.

Model the Dictionary as an Unweighted Graph

Assume all words have the same length. Then each step is constrained to a one-character substitution, so every edge has equal cost. That makes the search an unweighted shortest-path problem.

For example, with hit as the start and cog as the goal, one valid sequence is:

text
hit -> hot -> dot -> dog -> cog

Because every transformation costs one step, BFS is ideal. It explores words in layers:

  • all words one move away from the start
  • then all words two moves away
  • then all words three moves away

The first time BFS reaches the target, it has found a shortest transformation.

Straightforward BFS

A basic implementation generates neighbors by trying every letter in every position. For a word of length m, that means up to 26 * m candidates per expansion, which is fine for moderate input sizes.

The version below returns the actual path instead of only its length:

python
1from collections import deque
2
3
4def word_ladder(begin_word, end_word, word_list):
5    words = set(word_list)
6    if end_word not in words:
7        return []
8
9    queue = deque([begin_word])
10    parent = {begin_word: None}
11
12    while queue:
13        word = queue.popleft()
14        if word == end_word:
15            break
16
17        letters = list(word)
18        for i in range(len(letters)):
19            original = letters[i]
20            for ch in "abcdefghijklmnopqrstuvwxyz":
21                if ch == original:
22                    continue
23                letters[i] = ch
24                candidate = "".join(letters)
25                if candidate in words and candidate not in parent:
26                    parent[candidate] = word
27                    queue.append(candidate)
28            letters[i] = original
29
30    if end_word not in parent:
31        return []
32
33    path = []
34    current = end_word
35    while current is not None:
36        path.append(current)
37        current = parent[current]
38
39    return list(reversed(path))
40
41
42dictionary = ["hot", "dot", "dog", "lot", "log", "cog"]
43print(word_ladder("hit", "cog", dictionary))

Output:

text
['hit', 'hot', 'dot', 'dog', 'cog']

Faster Neighbor Lookup with Pattern Buckets

When the dictionary is large, generating all 26 * m variants for every word can still be wasteful. A common optimization is to precompute wildcard patterns.

For dog, the wildcard patterns are:

  • '*og'
  • 'd*g'
  • 'do*'

Every word that shares one of those patterns is a valid one-step neighbor. Building a map from pattern to words lets you jump directly to possible transformations.

Here is a compact version:

python
1from collections import defaultdict, deque
2
3
4def build_patterns(words):
5    buckets = defaultdict(list)
6    for word in words:
7        for i in range(len(word)):
8            pattern = word[:i] + "*" + word[i + 1 :]
9            buckets[pattern].append(word)
10    return buckets
11
12
13def shortest_length(begin_word, end_word, word_list):
14    words = set(word_list)
15    words.add(begin_word)
16    if end_word not in words:
17        return 0
18
19    buckets = build_patterns(words)
20    queue = deque([(begin_word, 1)])
21    seen = {begin_word}
22
23    while queue:
24        word, distance = queue.popleft()
25        if word == end_word:
26            return distance
27
28        for i in range(len(word)):
29            pattern = word[:i] + "*" + word[i + 1 :]
30            for neighbor in buckets[pattern]:
31                if neighbor not in seen:
32                    seen.add(neighbor)
33                    queue.append((neighbor, distance + 1))
34
35    return 0

This does more preprocessing up front, but it reduces repeated neighbor generation and is often the cleaner choice for interview-style or production-grade implementations.

Why BFS Is Correct

DFS can find a path, but not necessarily the shortest one. Dynamic programming is not a natural fit because the graph can contain many overlapping paths and cycles are prevented only by visited tracking, not by a simple recurrence.

BFS works because each edge has equal weight. The first time a word leaves the queue, the algorithm has already found the minimum number of steps needed to reach it.

If you need even better performance on very large dictionaries, bidirectional BFS is the next upgrade. It searches forward from the start and backward from the end, meeting somewhere in the middle. That often reduces the number of explored states dramatically.

Common Pitfalls

One common mistake is forgetting to check whether the target word exists in the dictionary. If it does not, there is no valid transformation under the usual problem definition.

Another bug is marking words as visited too late. If you wait until dequeue time rather than enqueue time, the same word may enter the queue multiple times, which wastes memory and can complicate path reconstruction.

Be careful with mixed word lengths. The standard algorithm assumes all words have the same size. If they do not, the one-letter substitution graph is not defined the same way, and you need a different model that allows insertion or deletion.

Finally, decide whether the start word must be in the dictionary. Many versions of the problem do not require it, so adding it to the working set is usually the simplest choice.

Summary

  • The word transformation problem is a shortest-path search on an implicit graph.
  • BFS is the standard solution because every valid transformation has equal cost.
  • You can return the path itself by storing a parent map during the search.
  • Wildcard pattern buckets speed up neighbor lookup for large dictionaries.
  • Correct visited handling and consistent word length assumptions are essential for a reliable implementation.

Course illustration
Course illustration

All Rights Reserved.