Word Ladder

https://leetcode.com/problems/word-ladder/

Given two words (start and end), and a dictionary, find the length of shortest transformation sequence from start to end, such that:

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

For example,

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

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

Note:

    • Return 0 if there is no such transformation sequence.
    • All words have the same length.
    • All words contain only lowercase alphabetic characters.

解题思路:

先分析题意。从start到end,每次只能改一个字母,要求改完的词都在dict中。有点像dfs,我可以每次从dict里找一个词,同时维护一个visited的set,用来判断这个词是不是已经被使用过了。如果,这个词没有被使用过,而且与前一个词只相差一个字母,就可以继续拓展,否则剪枝。这样直到可以抵达end,回溯。我们将所有结果都加入一个结果集。最后算这个结果集里面最小的那个就行了。

代码如下。

public class Solution {

    public int ladderLength(String start, String end, Set<String> dict) {

        Set<String> visited = new HashSet<String>();

        visited.add(start);

        List<String> current = new ArrayList<String>();

        current.add(start);

        List<Integer> result = new ArrayList<Integer>();

        dfs(end, dict, visited, current, result);

        if(result.size() == 0) {

            return 0;

        }

        Collections.sort(result);

        return result.get(0);

    }

    

    public void dfs(String end, Set<String> dict, Set<String> visited, List<String> current, List<Integer> result) {

        if(differsOnlyOne(current.get(current.size() - 1), end)) {

            result.add(current.size() + 1);

            return;

        }

        for (String str : dict) {

            if(differsOnlyOne(current.get(current.size() - 1), str) && !visited.contains(str)) {

                current.add(str);

                visited.add(str);

                dfs(end, dict, visited, current, result);

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

                visited.remove(str);

            }

        }

    }

    

    public boolean differsOnlyOne(String str1, String str2) {

        int differNum = 0;

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

            if(str1.charAt(i) != str2.charAt(i)) {

                differNum++;

            }

        }

        if(differNum == 1) {

            return true;

        }else {

            return false;

        }

    }

}

DFS,结果超时+内存溢出。因为本题要求的最小步数,无论如何都会感觉到brute dfs有些不对,因为肯定没必要把每个可能的解都算出来。前面提到过,一般求最优解的,可以使用动态规划,这里似乎也不行。

而且,使用上面的dfs,令dict的size为n,对于每个词,都有n个拓展的可能,时间复杂度会比较高。

看了tag,提示使用BFS。想想有道理,因为求的是最小的步数,相当于是最浅的高度,或者是最短路径!回忆一下前面经常写的BFS,几层循环相套。第一层是可能的最长路径,第二层是当前存在的解的个数,第三层循环是针对每个接继续向下拓展。

public class Solution {

    public int ladderLength(String start, String end, Set<String> dict) {

        List<List<String>> result = new ArrayList<List<String>>();

        List<String> current = new ArrayList<String>();

        current.add(start);

        result.add(current);



        int num = 1;

        for (int i = 0; i < dict.size(); i++) {

            List<List<String>> temp = new ArrayList<List<String>>();

            for (List<String> list : result) {

                if(differsOnlyOne(list.get(list.size() - 1), end)) {

                    return list.size() + 1;

                }

                for (String str : dict) {

                    List<String> temp1 = new ArrayList<String>(list);

                    if(differsOnlyOne(temp1.get(temp1.size() - 1), str) && !temp1.contains(str)) {

                        temp1.add(str);

                        temp.add(temp1);

                    }

                }

            }

            result = temp;

        }

        return 0;

    }

    

    public boolean differsOnlyOne(String str1, String str2) {

        int differNum = 0;

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

            if(str1.charAt(i) != str2.charAt(i)) {

                differNum++;

            }

        }

        if(differNum == 1) {

            return true;

        }else {

            return false;

        }

    }

}

结果换成BFS,还是超时。网上有人说,最内层的针对set的循环是非常耗时间的,如果set很大的话。不如对当前word的每位换字符,这样因为只有26个字母,word的长度也是恒定的,这个耗时就是一个常数的时间,而不会因为set过大而增长。结果,还是超时,代码就不贴了。

最后,AC的解法是下面的样子的。写出来才恍然大悟,其实就和二叉树的bfs一模一样。用了一个size的变量来记录当前已经遍历到第几层。

public class Solution {

    public int ladderLength(String start, String end, Set<String> dict) {

        Set<String> visited = new HashSet<String>();

        LinkedList<String> queue = new LinkedList<String>();

        queue.offer(start);

        visited.add(start);

        

        int level = 1;

        int size = queue.size();

        while (queue.size() > 0) {

            size = queue.size();

            while(size > 0) {

                String last = queue.poll();

                for(int j = 0; j < last.length(); j++) {

                    for(char k = 'a'; k <= 'z'; k++) {

                        String newString = last.substring(0, j) + k + last.substring(j + 1);

                        if(newString.equals(end)) {

                            return level + 1;

                        }

                        if(dict.contains(newString) && !visited.contains(newString)) {

                            visited.add(newString);

                            queue.offer(newString);

                        }

                    }

                }

                size--;

            }

            level++;

        }

        return 0;

    }

}

上面的解法是AC了,不过还有两个问题。

第一,这个visited的set是全局用的,难道不是应该每个路径一个set吗?因为逻辑上来讲,只要这条路径前面没用过这个词就行了,其他路径用过的,当前为啥排除了也能得到正确的解?我们假设start=abc,往下一层有adc、aec、afc等,再往下,adc可以变成aec,因为aec没有在abc-adc里出现过,但是必然不可能是最短解。因为abc可以直接到aec,已经比adb-adc-aec短了。

所以这个visited的set完全可以所有路径共用一个。再想想,其实这就是图的BFS方法。一个图,能直接到的点,肯定是比从其他点绕一圈,再过来要短的。所谓,不走回头路,也不要绕路。

第二,第一次写的BFS是维护了一个List<List<String>>的类型的,因为以前都是要BFS出所有结果,所以要保留前面所有的路径前驱。而这里,只要求解的深度就行了。所以其实只要像树的bfs那样,用一个queue,维护最近一次的所有前一个结点即可。前面所有的前驱放在visited这个set里就可以了。

第一点,对于问题的解决是关键,可以说大大降低了问题的时间复杂度。第二点,可以使得空间复杂度降低。

我这里尝试着,用List<List<String>>的BFS,改进visited的set,而不用queue,也是可以AC的。代码如下。

说明时间复杂度的关键,第一,在针对每个词进行拓展的时候,不要遍历整个dict,而要每个位置的每个字母进行拓展,因为可能性只有26种。第二,就是这个set。

public class Solution {

    public int ladderLength(String start, String end, Set<String> dict) {

        List<List<String>> result = new ArrayList<List<String>>();

        List<String> current = new ArrayList<String>();

        Set<String> visited = new HashSet<String>();

        visited.add(start);

        current.add(start);

        result.add(current);



        int num = 1;

        for (int i = 0; i < dict.size(); i++) {

            List<List<String>> temp = new ArrayList<List<String>>();

            for (List<String> list : result) {

                String last = list.get(list.size() - 1);

                if(differsOnlyOne(last, end)) {

                    return list.size() + 1;

                }

                for(int j = 0; j < last.length(); j++) {

                    for(char k = 'a'; k <= 'z'; k++) {

                        String newString = last.substring(0, j) + k + last.substring(j + 1);

                        List<String> temp1 = new ArrayList<String>(list);

                        if(dict.contains(newString) && !visited.contains(newString)) {

                            temp1.add(newString);

                            visited.add(newString);

                            temp.add(temp1);

                        }

                    }

                }

            }

            result = temp;

        }

        return 0;

    }

    

    public boolean differsOnlyOne(String str1, String str2) {

        int differNum = 0;

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

            if(str1.charAt(i) != str2.charAt(i)) {

                differNum++;

            }

        }

        if(differNum == 1) {

            return true;

        }else {

            return false;

        }

    }

}

 

你可能感兴趣的:(word)