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:
- Only one letter can be changed at a time
- 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; } } }
//20180930
题目本身做了一些更改,首先参数不是set而是list了,其次,如果endWord不在wordList里面,也被认为是不可以的。
class Solution { public int ladderLength(String beginWord, String endWord, List<String> wordList) { Set<String> visited = new HashSet<String>(); Set<String> wordListSet = new HashSet<String>(wordList); if (!wordListSet.contains(endWord)) { return 0; } LinkedList<String> queue = new LinkedList<String>(); queue.offer(beginWord); int level = 1; // queue里面是所有层的所有可能的结果,他们之间的层次是通过sizeForThisLevel来得知的 while (queue.size() > 0) { int sizeForThisLevel = queue.size(); while (sizeForThisLevel > 0) { // 对于本层的所有词(所有的候选词),尝试所有的变换 // 所以这里的lastword要不断的poll,直到sizeForThisLevel == 0. String lastWord = queue.poll(); for (int i = 0; i < lastWord.length(); i++) { for (char c = 'a'; c <= 'z'; c++) { String candidate = lastWord.substring(0, i) + c + lastWord.substring(i + 1, lastWord.length()); if (candidate.equals(endWord)) { return level + 1; } if (!visited.contains(candidate) && wordListSet.contains(candidate)) { queue.offer(candidate); visited.add(candidate); } } } sizeForThisLevel--; } // sizeForThisLevel == 0 代表这里层都试完了还没有结果,进入下一层尝试。 level++; } return 0; } }
另一种方法
class Solution { public int ladderLength(String beginWord, String endWord, List<String> wordList) { Set<String> visited = new HashSet<String>(); Set<String> wordListSet = new HashSet<String>(wordList); if (!wordListSet.contains(endWord)) { return 0; } List<String> cur = new ArrayList<String>(); List<List<String>> res = new ArrayList<List<String>>(); cur.add(beginWord); visited.add(beginWord); res.add(cur); // 最多的变换长度就是 wordList.size() for (int level = 0; level < wordList.size(); level++) { // temp就是这一层穷举后所有可能的candidate变换list // 所以从空开始,然后用上一层的穷举结果res继续往下尝试 List<List<String>> temp = new ArrayList<List<String>>(); for (List<String> list : res) { String lastWord = list.get(list.size() - 1); for (int i = 0; i < lastWord.length(); i++) { for (char c = 'a'; c <= 'z'; c++) { String candidate = lastWord.substring(0, i) + c + lastWord.substring(i + 1, lastWord.length()); if (candidate.equals(endWord)) { return list.size() + 1; } if (!visited.contains(candidate) && wordListSet.contains(candidate)) { List<String> cur2 = new ArrayList<String>(list); cur2.add(candidate); temp.add(cur2); visited.add(candidate); } } } } // 本层的穷举结果temp取代上一层的结果res res = temp; } return 0; } }