LeetCode – Word Ladder

Given two words (start and end), and a dictionary, find the length of shortest transformation sequence from start to end, such that only one letter can be changed at a time and each intermediate word must exist in the dictionary. For example, given:

start = "hit"
end = "cog"
dict = ["hot","dot","dog","lot","log"]

One shortest transformation is “hit” -> “hot” -> “dot” -> “dog” -> “cog”, the program should return its length 5.

Java Solution

This is a search problem, and breath-first search guarantees the optimal solution.

word-ladder

class WordNode{
    String word;
    int numSteps;
 
    public WordNode(String word, int numSteps){
        this.word = word;
        this.numSteps = numSteps;
    }
}
 
public class Solution {
    public int ladderLength(String beginWord, String endWord, Set<String> wordDict) {
        LinkedList<WordNode> queue = new LinkedList<WordNode>();
        queue.add(new WordNode(beginWord, 1));
 
        wordDict.add(endWord);
 
        while(!queue.isEmpty()){
            WordNode top = queue.remove();
            String word = top.word;
 
            if(word.equals(endWord)){
                return top.numSteps;
            }
 
            char[] arr = word.toCharArray();
            for(int i=0; i<arr.length; i++){
                for(char c='a'; c<='z'; c++){
                    char temp = arr[i];
                    if(arr[i]!=c){
                        arr[i]=c;
                    }
 
                    String newWord = new String(arr);
                    if(wordDict.contains(newWord)){
                        queue.add(new WordNode(newWord, top.numSteps+1));
                        wordDict.remove(newWord);
                    }
 
                    arr[i]=temp;
                }
            }
        }
 
        return 0;
    }
}

58 thoughts on “LeetCode – Word Ladder”

  1. So my thought, if the dictionary isn’t that big, provide all options of the dictionary words with one letter missing.
    This way, we can find the matching “1-letter changed” quickly – without traversing through all the abc.
    Such a dictionary from the example would be:
    [-ot, h-t, ho-, -ot, d-t, do-, -og, d-g, do-, -ot, l-t, lo-, -og, l-g, lo-].
    We still need to save the original word so we’ll add it to the dictionary.
    If we have the space, and can partition the dictionary based on string length, the iteration over the abc would be faster – O(size-of-word) instead of O(abc…z)=O(26).

    What do you think?

  2. So the solution I see iterates over all the “abc…z” – for each letter in each word in the queue. On each iteration, the new built word is compared to all dictionary words, using Set.contains().
    I was thinking, perhaps it might be faster to compare letter by letter.
    But the set, especially if it’s a HashSet, will do the contains much faster because of the String’s hashCode().
    Just sharing a thought 🙂


  3. const shortestPath = (start, end, dict, path=[]) => {
    if (start === end) {
    return 2
    }

    let results = []
    for (var i = 0; i < start.length; i++ ) {
    for (var j ='a'.charCodeAt(0); j -1 && path.indexOf(next) === -1) {
    results.push(shortestPath(next, end, dict, path.concat(next)) + 1)
    } else if (next === end) {
    return 2
    }
    }
    }
    return Math.min(...results)
    }

  4. defining distance between words to be number of letters which are different then we start like this

    add initial into processing queue
    add end to dictionary
    dequeuing from processing queue
    compute distance from item to each in the dictionary O(Dict)
    add to queue the ones with distance one and remove them from dict so that you don’t loop

    If the dictionary can be prepressed then you can organize it as a graph with arches connecting elements which have dinstance 1.

    Then is just a matter of breath-first search and display the result.

  5. You have to check the newWord with the endWord

    String newWord = new String(arr);
    if (dict.contains(newWord)) {
    System.out.println(newWord);
    queue.add(new WordNode(newWord, top.numSteps + 1));
    dict.remove(newWord);
    // you have to add the next lines.
    if (newWord.equals(end)) {
    // System.out.println("equals!");
    return top.numSteps;
    }
    }

  6. // “hit” -> “cit” -> “cot” -> “cog” => every intermediate word must exist in the dictionary, but “cit” doesn’t.

  7. I have Solved this problem in my way.Can please anyone locate the use case in which it will break

    package com.cpa.examples;

    import java.util.ArrayList;
    import java.util.HashSet;
    import java.util.List;

    public class WordLadder {
    private static String start = “hit”;
    private static String end = “cog”;
    private static List Dictionary = null;
    private static ArrayList availablePathCntList = new ArrayList();

    public static void main(String[] args) {

    Dictionary = new ArrayList();

    Dictionary.add(“hot”);
    Dictionary.add(“dot”);
    Dictionary.add(“dog”);
    Dictionary.add(“lot”);
    Dictionary.add(“log”);
    Dictionary.add(“cog”);

    HashSet used = new HashSet();
    used.add(start);

    int count = 0;

    find(Dictionary, start, end, used, count);

    System.out.println(availablePathCntList);

    }

    private static int find(List dictionary, String start, String end, HashSet used, int count) {

    if (start == end) {
    availablePathCntList.add(count);
    return 0;

    }
    if (difference(start, end) == 1) {
    availablePathCntList.add(count + 1);
    return 0;
    }

    for (String word : dictionary) {

    int diff = difference(start, word);
    if (difference(end, word) == 0) {
    availablePathCntList.add(1);
    continue;
    }
    if (diff == 0) {
    availablePathCntList.add(count);
    continue;
    }
    if (diff <= 1 && used.add(word)) {
    System.out.println("Your word is: " + start + " and matching word is " + word);

    if (diff <= 1) {
    if (difference(end, word) == 1) {
    availablePathCntList.add(count + 1);
    } else {
    count++;
    find(Dictionary, new String(word), new String(end), (HashSet) used.clone(), count);
    }

    }
    }

    }
    return 0;

    }

    private static int difference(String first, String second) {

    char[] firstArr = first.toCharArray();
    char[] secondArr = second.toCharArray();
    int mismatch = 0;

    for (int i = 0; i < firstArr.length; i++) {

    if (firstArr[i] != secondArr[i]) {
    mismatch++;
    }
    }

    return mismatch;
    }

    }

  8. This can definitely be better. You should include visited words and make sure you are not adding visited words back to the graph.

  9. for the same input as yours with dictionary changed to below:

    dict = [“hot”,”dot”,”dog”,”lot”,”aog”,”log”]

    answer will change like : “hit” -> “hot” -> “dot” -> “dog” -> “aog”->”cog”,

    but from dog to cog also we can go directly, how will you avoid that?

  10. Solution just using 2 HashMaps and 1 ArrayList

    package leetcode;

    import java.util.ArrayList;

    import java.util.HashMap;

    import java.util.List;

    import java.util.Map;

    public class Solution {

    private static int min = Integer.MAX_VALUE;

    private static List visitedkeys = new ArrayList();

    public void wordladder(String start, String end, String[] dict) {

    //Word ladder

    Map map2 = new HashMap();

    Map<String, List> map = new HashMap();

    int i=0;

    map2.put(i++, start);

    map.put(start, new ArrayList());

    for (String d : dict) {

    map2.put(i++, d);

    map.put(d, new ArrayList());

    }

    map2.put(i, end);

    map.put(end, new ArrayList());

    int n = map2.size();

    for (i=0; i < n; i++) {

    for (int j=0; j < n; j++) {

    if (isConvertible(map2.get(i), map2.get(j))) {

    map.get(map2.get(i)).add(map2.get(j));

    }

    }

    }

    length(map, start, end, 1);

    if (min != Integer.MAX_VALUE){

    System.out.println(min);

    }

    else {

    System.out.println("no path");

    }

    }

    private boolean isConvertible(String s1, String s2) {

    if (s1.length() != s2.length()) {

    return false;

    }

    int diff = 0;

    for (int i = 0; i < s1.length(); i++) {

    if (s1.charAt(i) != s2.charAt(i)) {

    diff += 1;

    }

    }

    return (diff == 1) ? true : false;

    }

    private void length(Map<String, List> map, String key, String end, int len) {

    if (key == end) {

    if (len < min)

    min = len;

    return;

    }

    List lst = map.get(key);

    for (String v : lst) {

    if (!visitedkeys.contains(v)){

    visitedkeys.add(v);

    length(map, v, end, len+1);

    visitedkeys.remove(visitedkeys.size()-1);

    }

    }

    }

    }

  11. Solution posted in the task is not correct.

    Here is mine:

    Test.java file:

    import java.util.*;
    public class Test {
    public static void main(String argv[]){
    String start = "hit";
    String end = "cog";
    String[] dict = {"cot","hot","dot","cit","dog","lot","log"};
    // "hit" -> "cit" -> "cot" -> "cog"
    // length = 4
    int minDistance = getDistance(start, end);
    List nodesToCheck = new ArrayList();
    List newNodes = new ArrayList();
    List winners = new ArrayList();
    nodesToCheck.add(new TreeElem(start, null, minDistance, 1));
    do {
    for (TreeElem node : nodesToCheck) {
    for (int i = 0; i 0);
    if (winners.size() == 0) {
    System.out.println("No solution found.");
    return;
    }
    TreeElem winner = null;
    int minLength = -1;

    for (TreeElem e : winners) {
    if (winner == null) {
    winner = e;
    minLength = e.distanceFromTop;
    }

    if (e.distanceFromTop = 0; i--) {
    System.out.print("[" + steps[i] + "] -> ");
    }
    System.out.println("[" + end + "]");
    }
    public static int getDistance(String str1, String str2) {
    int dist = 0;
    for (int i = 0; i < str1.length(); i++) {
    if (str1.charAt(i) != str2.charAt(i)) {
    dist++;
    }
    }
    return dist;
    }
    public static boolean isUsed(TreeElem node, String value) {
    while (node.parent != null) {
    if (node.value.equals(value)) {
    return true;
    }
    node = node.parent;
    }
    return false;
    }
    }

    TreeElem.java file:


    import java.util.*;
    public class TreeElem{
    public String value;
    public TreeElem parent;
    List childs;
    public int distanceToEnd;
    public int distanceFromTop;
    public TreeElem(String value, TreeElem parent, int distanceToEnd, int distanceFromTop) {
    this.value = value;
    this.parent = parent;
    this.childs = new ArrayList();
    this.distanceToEnd = distanceToEnd;
    this.distanceFromTop = distanceFromTop;
    }
    public void addChild(TreeElem child) {
    childs.add(child);
    }
    }

  12. Without using a queue. Please let me know if you see any issues with this?

    public int getCount(String start, String end, Set dict){

    // Basic edge cases

    if(start==null || end==null || start.length()!=end.length()) return -1;

    if(dict.isEmpty()) return -1;

    dict.add(end);

    int count = 0;

    int i = 0;

    while(i<start.length()){

    boolean found = false;

    char[] arr = start.toCharArray();

    char tmp = arr[i];

    for(int j=97;j<=122;j++){

    arr[i] = (char)j;

    String checker = new String(arr);

    if(dict.contains(checker)){

    count++;

    dict.remove(checker);

    if(checker.equals(end)) {

    return count;

    }

    else {

    start = checker;

    found = true;

    i = 0;

    break;

    }

    }

    }

    if(!found){

    arr[i] = tmp;

    i++;

    }

    }

    return -1;

    }

  13. Hi
    I have solved this problem using Graph of word and then run on it BFS.

    package Questions;

    import java.util.HashMap;
    import java.util.Queue;
    import java.util.HashSet;
    import java.util.LinkedList;

    public class WordLadder {

    //solution function
    public static int ladderLength(String start, String end,
    HashSet dict) {
    // saving the graph nodes on hash map.
    HashMap nodes = new HashMap();
    // adding the start and the end words to the dict
    dict.add(start);
    dict.add(end);
    for (String word : dict) {
    nodes.put(word, new GraphNode(word));
    }
    // update each node’s adjacents according to one character different relation
    Object[] dictArray = dict.toArray();
    for (int i = 0; i < dictArray.length; i++) {
    for (int j = i + 1; j < dictArray.length; j++) {
    if (isNeighbor((String) dictArray[i], (String) dictArray[j])) {
    nodes.get((String) dictArray[i]).childs.add(nodes
    .get((String) dictArray[j]));
    nodes.get((String) dictArray[j]).childs.add(nodes
    .get((String) dictArray[i]));
    }
    }
    }

    // Run BFS on the Graph and take the dist generated as result
    HashMap result = BFS(nodes, start);

    // Return the distance of the end word node from the start word node
    return result.get(end);

    }

    // BFS function
    public static HashMap BFS(
    HashMap nodes, String start) {

    HashMap visited = new HashMap();
    HashMap dist = new HashMap();
    for (String key : nodes.keySet()) {
    visited.put(key, 0);
    dist.put(key, 0);
    }
    Queue q = new LinkedList();
    q.add(start);
    visited.put(start, 1);
    while (!q.isEmpty()) {
    String dequeued = q.remove();
    GraphNode curNode = nodes.get(dequeued);
    LinkedList currAdjs = curNode.childs;
    for (int i = 0; i < currAdjs.size(); i++) {
    GraphNode adj = (GraphNode) currAdjs.get(i);
    if (visited.get(adj.word) == 0) {
    visited.put(adj.word, 1);
    dist.put(adj.word, dist.get(dequeued) + 1);
    q.add(adj.word);
    }

    }
    }
    return dist;

    }

    // check if two words differ by one character
    public static boolean isNeighbor(String a, String b) {
    assert a.length() == b.length();
    int differ = 0;
    for (int i = 0; i 1)
    return false;
    }
    return true;
    }

    public static void main(String[] args) {
    // dict = [“hot”,”dot”,”dog”,”lot”,”log”] result 5;
    HashSet dict = new HashSet();
    dict.add(“hot”);
    dict.add(“dot”);
    dict.add(“dog”);
    dict.add(“lot”);
    dict.add(“log”);
    System.out.println(ladderLength(“hit”, “cog”, dict));
    }

    }

    class GraphNode {
    String word;
    LinkedList childs;

    public GraphNode(String word) {
    this.word = word;
    childs = new LinkedList();
    }
    }
    I would like to have feedback on my code ?
    Thanks

  14. You also don’t necessarily need two queues. You could create a WordNode class inside the method and have a word field and a distance field. Then you can create a queue of “WordNode”s. Also, I think there is no need to compare the distance to previously recorded one, since breadth search will reach the end word in the shortest path. Whenever you reach the end word, that is the shortest distance.

  15. shortest path problem with dictionary extended to include start and end and weight equals to 1 if distance(a, b) == 1, else weight equals infinite

  16. we stop at the first time we find the target word and return “length”. All the words that will be added in the stack after that will be > length as we push words in stack after length +1 .

  17. Hi! I think you were oversimplifying the problem. the edge cannot be weighted just based on the number of characters in common. And an edge cannot even be determined between two words just because they have common characters. You have to factor in what are included the dictionary. One simple example, ‘good’ and ‘doom’ have two common characters and suppose the dictionary only contains ‘good’ and ‘doom’…Apparently no edge of path could be established between the two words.

  18. Agree with most of the comments here:

    First: the dictionary here needs to include the end word

    Second: the second solution does not find the optimal shortest path, it only finds the first valid path.

    I have come up with my own solution which does the following:

    1. Instead of trying to iterate through all chars from a to z, it parses the dictionary and maintains a map between index and possible chars for that index in the dictionary, so flipping a character only flipped to possible dictionary words.

    2. recursive Graph depth first search which keeps tracks of all sub path size and choose the shortest one.

    Please kindly review and comment to help improve the algo

    https://github.com/ifwonderland/leetcode/blob/master/src/main/java/leetcode/string/WordLadder.java
    https://github.com/ifwonderland/leetcode/blob/master/src/main/java/leetcode/util/WordFlipperUtils.java

  19. I think there should be a check on the currWord and the end to stop the search in the dict:

     
    /*if(currWord.equals(end)){
       return currDistance;
     }*/
    if(isLastWord(currWord,end)){
                    break;
     }
    where isLastWord is something like this:
    
    private boolean isLastWord(String current, String end){
            int count = 0;
            boolean isLastWord = true;
            for(int i=0; i1){
                        isLastWord = false;
                        break;
                    }
                }
            }
            return isLastWord;
        }
    
  20. The second solution doesn’t work. Try with input: “hit”, “lag”, [“hot”,”hog”,”dot”,”dog”,”lot”,”log”]
    Example sequence: hit > hot > hog > log > lag The answer should be 5. But, it shows 0

  21. how it will accepted?? as it will not return as cog is not inside in dict, so currWord.equals(end) will false and return 0.

  22. How can I print the world ladder answers, after getting the distance.? Thanks for the answers.

  23. This should be a shortest path problem: if two words has common character, they are connected by edge. The weight of the edge is the number of character in common. So the problem becomes shortest path problem.

  24. The second algo has a bug. What the problem says is that “Each intermediate word must exist in the dictionary”. However, this solution assumes the last one(“cog”) also need to be in the dict, which is not necessary..

Leave a Comment