Start learning free

LESSON 366 OF 414 · GRAPHS

Word Ladder in Scala: Breadth-First Search with a List Scan

Practice O(N2 * L) Read the full explanation

Given two words (beginWord and endWord), and a list of words (words), find the length of shortest transformation sequence from beginWord to endWord, such that:

  1. Only one letter can be changed at a time.
  2. Each transformed word must exist in the word list.

Note:

  • Return 0 if there is no such transformation sequence.
  • All words have the same length.
  • All words contain only lowercase alphabetic characters.
  • You may assume no duplicates in the word list.
  • You may assume beginWord and endWord are non-empty and are not the same.

Example 1:

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

Output: 5

Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.

Example 2:

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

Output: 0

Explanation: The endWord "cog" is not in words, therefore no possible transformation.
STEP 1 OF 16
    Ctrl + Enter to check

    SCALA · GRAPHS

    Word Ladder in Scala, explained

    Problem overview

    The method ladderLength receives a beginWord, an endWord and an Array[String] called words. It changes one letter at a time, and every word along the way must be in words. It returns the number of words in the shortest such sequence, counting both ends, or 0 when no sequence exists. All words have the same length and contain lowercase letters only.

    For "hit" to "cog" with ["hot","dot","dog","lot","log","cog"], the sequence hit, hot, dot, dog, cog has 5 words, so the answer is 5. When "cog" is missing from the list, nothing can end there and the answer is 0. Note that the answer counts words, not changes: four changes make a sequence of five words.

    Approach: a graph of words

    Treat every word as a vertex, and join two words with an edge when they differ in exactly one position. The task is then the shortest path between two vertices in an unweighted graph, and the standard tool for that is breadth-first search (BFS). BFS visits the vertices in order of distance from the start: first the start, then everything one change away, then everything two changes away, and so on. The first time it reaches a vertex, it has found a shortest route to it.

    This lesson builds the edges on the fly. For each word taken from the queue, it scans the whole list and tests every candidate with a helper that counts the positions where two strings differ. That is simple to write, and it is the slowest of the approaches to this problem. Later lessons replace the scan with faster neighbor lookups.

    A map from word to distance does two jobs. Its values are the answers, and its keys are the set of words already reached. A word is given its distance when it is put into the queue, not when it is taken out. That way a word cannot be queued twice.

    Step by step

    1. Create the queue as an empty scala.collection.mutable.Queue[String].
    2. Create minDist, a mutable HashMap[String, Int] from a word to the length of the shortest sequence that reaches it from beginWord.
    3. Start the search. Enqueue beginWord and set minDist(beginWord) to 1, because a sequence holding only the first word has length 1. Maps are written to with parentheses: minDist(key) = value.
    4. Wrap the loop in breakable. Scala has no break keyword. The break that this lesson uses comes from scala.util.control.Breaks and works by throwing an exception that the enclosing breakable block catches. The loop itself is while (queue.nonEmpty).
    5. Dequeue the front word into word.
    6. Stop at the target. If word equals endWord, call break. BFS removes words in order of distance, so the distance stored for endWord is already the shortest.
    7. Try every candidate. A for loop runs nextWord over words.
    8. Skip non-neighbors. Scala has no continue, so the loop body gets its own inner breakable block, and break in it skips only the current candidate. The helper numOfChanges returns the number of differing positions. Anything other than exactly 1 is skipped: 0 means the same word, and 2 or more is too far.
    9. Skip words already reached with minDist.contains(nextWord). They were found by a sequence at least as short.
    10. Record and enqueue. Otherwise minDist(nextWord) becomes minDist(word) + 1, and nextWord joins the queue.
    11. Return the answer with minDist.getOrElse(endWord, 0): the stored distance, or 0 if endWord was never reached.
    12. Write numOfChanges: a var differences starts at 0, a loop over 0 until first.length adds 1 whenever first(i) != second(i), and differences is the last expression.

    The solution in Scala

    class Solution {
        def numOfChanges(first: String, second: String): Int = {
            var differences = 0
            for (i <- 0 until first.length) {
                if (first(i) != second(i)) {
                    differences += 1
                }
            }
            differences
        }
    
        def ladderLength(beginWord: String, endWord: String, words: Array[String]): Int = {
            val queue = scala.collection.mutable.Queue[String]()
            val minDist = scala.collection.mutable.HashMap[String, Int]()
            queue.enqueue(beginWord)
            minDist(beginWord) = 1
            breakable {
                while (queue.nonEmpty) {
                    val word = queue.dequeue()
                    if (endWord == word) {
                        break
                    }
                    for (nextWord <- words) {
                        breakable {
                            if (numOfChanges(word, nextWord) != 1) {
                                break
                            }
                            if (minDist.contains(nextWord)) {
                                break
                            }
                            minDist(nextWord) = minDist(word) + 1
                            queue.enqueue(nextWord)
                        }
                    }
                }
            }
            minDist.getOrElse(endWord, 0)
        }
    }
    

    This code uses breakable and break, so the file needs import scala.util.control.Breaks._. The Scala details worth a closer look:

    • Nested breakable blocks. break leaves the innermost enclosing breakable. In the inner block it works like continue. In the outer block, break ends the BFS. Because the inner breakable sits inside the for, each candidate gets a fresh one.
    • Strings and characters. A string is indexed with parentheses, first(i), and gives a Char. Comparing two characters with != compares values.
    • No ++. Scala has no increment operator, so the counter uses differences += 1.
    • Map access. minDist(word) throws NoSuchElementException for a missing key, which is safe here because word always came out of the queue after being given a distance. getOrElse is used for the final lookup because the key may be absent.
    • Last expression. differences and the getOrElse call have no return in front of them. The last expression of a block is its value.

    Complexity

    Let N be the number of words and L their length.

    Time: O(N² * L). Each word enters the queue at most once. For every word taken out, the loop tests all N candidates, and each test compares up to L letters.

    Space: O(N * L). The queue and the map hold at most one entry per word, and each entry is a string of length L.

    Edge cases and common mistakes

    • endWord not in the list. It can never be queued, so the method returns 0.
    • beginWord not in the list. That is allowed. The search starts from it whether or not words holds it. For "hit" to "hot" with ["hot"], the answer is 2.
    • Counting changes instead of words. Starting minDist(beginWord) at 0 would return 4 for the main example instead of 5.
    • Treating 0 differences as a neighbor. A word compared with itself differs in 0 places. The test must be "exactly 1", not "at most 1".
    • Setting the distance on dequeue. The same word can then be queued many times before its first removal, which wastes work.
    • Breaking in the wrong block. A break placed directly in the for, outside the inner breakable, ends the whole BFS instead of skipping one candidate.

    Testing

    • "hit", "cog", ["hot","dot","dog","lot","log","cog"] gives 5.
    • "hit", "cog", ["hot","dot","dog","lot","log"] gives 0.
    • "hot", "dot", ["dot"] gives 2.
    • "a", "c", ["a","b","c"] gives 2.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free