\

Word Ladder

Difficulty: N/A | Solved: December 20, 2025

Problem

transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that:

  • Every adjacent pair of words differs by a single letter.
  • Every si for 1 <= i <= k is in wordList. Note that beginWord does not need to be in wordList.
  • sk == endWord

Given two words, beginWord and endWord, and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, or 0 if no such sequence exists.

Example 1:

Input: beginWord = “hit”, endWord = “cog”, wordList =["hot","dot","dog","lot","log","cog"] Output: 5 Explanation: One shortest transformation sequence is “hit” -> “hot” -> “dot” -> “dog” -> cog", which is 5 words long.

Example 2:

Input: beginWord = “hit”, endWord = “cog”, wordList = ["hot","dot","dog","lot","log"] Output: 0 Explanation: The endWord “cog” is not in wordList, therefore there is no valid transformation sequence.

Brainstorming

So we want to basically transform this problem from a word problem to a graph problem. We do that by essentially defining the nodes and the edges. The key here is to see each word as only one character way from the other word. For eg “hot” and “hit” are separated by one character that is i so the words that are in this relationship will have a edge between them. We are essentially trying to create an adjacency list like this


words_list = {
			  "hit": ["hot","dot", "lot"],
			  "hot" : ["hit", "dot", "lot"],
			  "dot" : ["hot", "lot", "dog"],
			  "dog" : ["log", "cog", "dot"],
			  "log" : ["cog", "dog", "lot"],
			  }

But how do we do that. One way is to change each character of the start word with all the letters in the alphabet and then see if they are in the world list

  1. Create a hash set of the original word list
  2. create a visited hash set
  3. take your start word add it queue
  4. add this word to your visited hash set
  5. start your while loop
  6. deque your word
  7. now using brute force try to find its neighbours
    1. HIT start with H replace it with all the characters starting from A all the way to Z
    2. check if the new word is in the word list if yes add it to its neighbours
    3. then we try with O and T we add all of them to the neighbours of HIT
    4. we add all the neighbours to the queue after we make sure they are already not in visited , we add them to visited too.
words_list = ["hot","dot","dog","lot","log","cog"]

the best part is we don’t have to play favourites , we can explore all possible neighbours of each word that we process

but going through the word list in a queue is not enough we also need to know how many words did it take to reach till there. so for each word that we find we can update a variable say distance that gets added and then that can be made part of a tuple with the word processing.

if we hit the end word we can just return the distance + 1 calculated so far. if the queue is empty and we don’t reach our end word it means there is no path we can traverse from start word to end word hence we can return an illegal value say -1

Let’s Refine Two Subtle Details to Make it Bulletproof

Your logic is sound. Let’s just clarify two small points to turn your description into a flawless implementation plan.

Subtle Detail 1: The Initial State

When you start, your queue needs its first item. Based on your logic, this would be:

  • queue.append(("HIT", 1)) We start the distance at 1, not 0. This is because the problem asks for the length of the sequence (how many words are in it), not the number of steps (which would be length - 1). So, “HIT” itself is the 1st word in the sequence. This makes the final return value very clean.

Subtle Detail 2: The visited Set Timing

This is a classic BFS optimisation that you touched upon. When is the absolute best time to add a word to the visited set?

  • Good: Add it when you pop it from the queue. This works, but it’s slightly inefficient.
  • Better: Add it just before you add it to the queue.

Why is this better? Imagine word_A can be reached from both word_B and word_C, which are in the same “layer.” If you only mark word_A as visited when you pop it, both B and C will add word_A to the queue. You’ll end up with duplicate work. By marking it as visited the moment you discover it, you ensure a word can only ever enter the queue once.

The Final Algorithm (Based on Your Logic)

Your description translates directly into this high-level plan:

  1. Setup:

    • Create a word_set from the word_list for O(1) lookups.
    • Initialize a queue with the tuple (start_word, 1).
    • Initialize a visited_set and add start_word to it.
  2. BFS Loop:

    • while queue is not empty:
      • current_word, distance = queue.popleft()
      • if current_word == end_word:
        • return distance (We found it!)
      • Find Neighbors:
        • Generate all possible one-letter-different words for current_word.
        • For each new_word:
          • if new_word in word_set and new_word not in visited_set:
            • visited_set.add(new_word)
            • queue.append((new_word, distance + 1))
  3. Failure:

    • If the loop finishes (queue is empty), it means we never found a path.
    • return 0 (or -1, as the problem specifies).

below is the python implementation

from collections import deque
import string


beginWord = "hit"
endWord = "cog"

words = ["hot","dot","dog","lot","log","cog"]
words_set = set(words)

def generate_word_candidates(word: str) -> list[str]:
    """
    Generates all possible one-letter-different words for a given word.
    Example: 'cat' -> ['aat', 'bat', 'cat'...'zat', 'cbt', 'cct'...'czt', 'caa', 'cab'...'caz']
    """
    candidates = []
    # Outer loop: Iterate through each character position of the word
    for i in range(len(word)):
        # Inner loop: Try substituting each letter of the alphabet
        for alphabet_char in string.ascii_lowercase:
            # Create the new word by slicing and combining
            new_word = word[:i] + alphabet_char + word[i+1:]
            candidates.append(new_word)
    return candidates
	
def word_ladder_length(beginWord:str, endWord:str, word_set:list[str]) -> int
	
	if endWord not in word_set:
		return 0
	visited = set()
	queue = deque([(beginWord,1)])
	visited = {beginWord}
		
	while queue:
		current_word, distance = queue.popleft()
		if current_word == endWord:
			return distance
		else:
			candidates = generate_word_candidate(current_word, words_set)
			for candidate in candidates:
				 if candidate in word_set and candidate not in visited:
					 visited.add(candidate)
					 queue.append((candidate, distance+1))

	return 0 


		

you can read more about python slicing in [[Python Slicing]]

ideal code


from collections import deque
import string

# Helper function is perfect, no changes needed here.
def generate_word_candidates(word: str) -> list[str]:
    """
    Generates all possible one-letter-different words for a given word.
    """
    candidates = []
    for i in range(len(word)):
        for alphabet_char in string.ascii_lowercase:
            new_word = word[:i] + alphabet_char + word[i+1:]
            candidates.append(new_word)
    return candidates

# The main function with all fixes applied.
def word_ladder_length(beginWord: str, endWord: str, wordList: list[str]) -> int:
    # --- SETUP ---
    word_set = set(wordList)
    if endWord not in word_set:
        return 0

    # FIX: All variables are now local to the function.
    queue = deque([(beginWord, 1)])
    visited = {beginWord} # A set automatically handles the start word.

    # --- BFS LOOP ---
    while queue:
        current_word, distance = queue.popleft()

        if current_word == endWord:
            return distance

        # FIX: The function call now matches the definition.
        candidates = generate_word_candidates(current_word)
        
        for candidate in candidates:
            if candidate in word_set and candidate not in visited:
                # FIX: Use .add() for sets.
                visited.add(candidate)
                queue.append((candidate, distance + 1))
    
    # If the loop finishes, no path was found.
    return 0

# --- Let's run it! ---
beginWord = "hit"
endWord = "cog"
words = ["hot","dot","dog","lot","log","cog"]

# We call the main function, passing in the word list.
final_length = word_ladder_length(beginWord, endWord, words)
print(f"The length of the shortest transformation is: {final_length}")