题目描述(中等难度)
给定一个开始单词和一个结束单词,一个单词列表,两个单词间转换原则是有且仅有一个字母不同。求出从开始单词转换到结束单词的最短路径长度是多少。
思路分析
基本上就是 126 题 的简化版了,可以先看一下 126 题 的解法思路。接下来就按照 126 题的思路讲了。把之前的图贴过来。
要求最短的路径,DFS
肯定在这里就不合适了。而在之前我们用 BFS
将每个节点的邻接节点求了出来,这个过程其实我们相当于已经把最短路径的长度求出来了。所以我们只需要把之前的 BFS
拿过来稍加修改即可。
解法一 BFS
添加一个变量 len
,遍历完每一层就将 len
加 1
。
public int ladderLength(String beginWord, String endWord, List<String> wordList) { if (!wordList.contains(endWord)) { return 0; } int len = 0; Queue<String> queue = new LinkedList<>(); queue.offer(beginWord); boolean isFound = false; Set<String> dict = new HashSet<>(wordList); Set<String> visited = new HashSet<>(); visited.add(beginWord); while (!queue.isEmpty()) { int size = queue.size(); Set<String> subVisited = new HashSet<>(); for (int j = 0; j < size; j++) { String temp = queue.poll(); // 一次性得到所有的下一个的节点 ArrayList<String> neighbors = getNeighbors(temp, dict); for (String neighbor : neighbors) { if (!visited.contains(neighbor)) { subVisited.add(neighbor); //到达了结束单词,提前结束 if (neighbor.equals(endWord)) { isFound = true; break; } queue.offer(neighbor); } } } //当前层添加了元素,长度加一 if (subVisited.size() > 0) { len++; } visited.addAll(subVisited); //找到以后,提前结束 while 循环,并且因为这里的层数从 0 计数,所以还需要加 1 if (isFound) { len++; break; } } return len; } private ArrayList<String> getNeighbors(String node, Set<String> dict) { ArrayList<String> res = new ArrayList<String>(); char chs[] = node.toCharArray(); for (char ch = 'a'; ch <= 'z'; ch++) { for (int i = 0; i < chs.length; i++) { if (chs[i] == ch) continue; char old_ch = chs[i]; chs[i] = ch; if (dict.contains(String.valueOf(chs))) { res.add(String.valueOf(chs)); } chs[i] = old_ch; } } return res; }
2020.6.22
更新,感谢 @cicada 指出,leetcode
增加了 case
,上边的代码不能 AC
了,我们需要考虑从第一个单词无法转到最后一个单词的情况,所以 return
前需要判断下。
public int ladderLength(String beginWord, String endWord, List<String> wordList) { if (!wordList.contains(endWord)) { return 0; } int len = 0; Queue<String> queue = new LinkedList<>(); queue.offer(beginWord); boolean isFound = false; Set<String> dict = new HashSet<>(wordList); Set<String> visited = new HashSet<>(); visited.add(beginWord); while (!queue.isEmpty()) { int size = queue.size(); Set<String> subVisited = new HashSet<>(); for (int j = 0; j < size; j++) { String temp = queue.poll(); // 一次性得到所有的下一个的节点 ArrayList<String> neighbors = getNeighbors(temp, dict); for (String neighbor : neighbors) { if (!visited.contains(neighbor)) { subVisited.add(neighbor); //到达了结束单词,提前结束 if (neighbor.equals(endWord)) { isFound = true; break; } queue.offer(neighbor); } } } //当前层添加了元素,长度加一 if (subVisited.size() > 0) { len++; } visited.addAll(subVisited); //找到以后,提前结束 while 循环,并且因为这里的层数从 0 计数,所以还需要加 1 if (isFound) { len++; break; } } if(isFound){ return len; }else{ return 0; } } private ArrayList<String> getNeighbors(String node, Set<String> dict) { ArrayList<String> res = new ArrayList<String>(); char chs[] = node.toCharArray(); for (char ch = 'a'; ch <= 'z'; ch++) { for (int i = 0; i < chs.length; i++) { if (chs[i] == ch) continue; char old_ch = chs[i]; chs[i] = ch; if (dict.contains(String.valueOf(chs))) { res.add(String.valueOf(chs)); } chs[i] = old_ch; } } return res; }
126 题 中介绍了得到当前节点的相邻节点的两种方案,官方题解 中又提供了一种思路,虽然不容易想到,但蛮有意思,分享一下。
就是把所有的单词归类,具体的例子会好理解一些。
一个单词会产生三个类别,比如 hot 会产生 *ot h*t ho* 然后考虑每一个单词,如果产生了相同的类,就把这些单词放在一起 假如我们的单词列表是 ["hot","dot","dog","lot","log","cog"] 考虑 hot,当前的分类结果如下 *ot -> [hot] h*t -> [hot] ho* -> [hot] 再考虑 dot,当前的分类结果如下 *ot -> [hot dot] h*t -> [hot] ho* -> [hot] d*t -> [dot] do* -> [dot] 再考虑 dog,当前的分类结果如下 *ot -> [hot dot] h*t -> [hot] ho* -> [hot] d*t -> [dot] do* -> [dot dog] *og -> [dog] d*g -> [dog] 再考虑 lot,当前的分类结果如下 *ot -> [hot dot lot] h*t -> [hot] ho* -> [hot] d*t -> [dot] do* -> [dot dog] *og -> [dog] d*g -> [dog] l*t -> [lot] lo* -> [lot] 然后把每个单词都放到对应的类中,这样最后找当前单词邻居节点的时候就方便了。 比如找 hot 的邻居节点,因为它可以产生 *ot, h*t, ho* 三个类别,所有它的相邻节点就是上边分好类的相应单词
解法二 双向搜索
在 126 题 最后一种解法中介绍了双向搜索,大大降低了时间复杂度。当然这里也可以直接用,同样是增加 len
变量即可,只不过之前用的递归,把 len
加到全局变量会更加方便些。
int len = 2; //因为把 beginWord 和 endWord 都加入了路径,所以初始化 2 public int ladderLength(String beginWord, String endWord, List<String> wordList) { if (!wordList.contains(endWord)) { return 0; } // 利用 BFS 得到所有的邻居节点 Set<String> set1 = new HashSet<String>(); set1.add(beginWord); Set<String> set2 = new HashSet<String>(); set2.add(endWord); Set<String> wordSet = new HashSet<String>(wordList); //最后没找到返回 0 if (!bfsHelper(set1, set2, wordSet)) { return 0; } return len; } private boolean bfsHelper(Set<String> set1, Set<String> set2, Set<String> wordSet) { if (set1.isEmpty()) { return false; } // set1 的数量多,就反向扩展 if (set1.size() > set2.size()) { return bfsHelper(set2, set1, wordSet); } // 将已经访问过单词删除 wordSet.removeAll(set1); wordSet.removeAll(set2); // 保存新扩展得到的节点 Set<String> set = new HashSet<String>(); for (String str : set1) { // 遍历每一位 for (int i = 0; i < str.length(); i++) { char[] chars = str.toCharArray(); // 尝试所有字母 for (char ch = 'a'; ch <= 'z'; ch++) { if (chars[i] == ch) { continue; } chars[i] = ch; String word = new String(chars); if (set2.contains(word)) { return true; } // 如果还没有相遇,并且新的单词在 word 中,那么就加到 set 中 if (wordSet.contains(word)) { set.add(word); } } } } //如果当前进行了扩展,长度加 1 if (set.size() > 0) { len++; } // 一般情况下新扩展的元素会多一些,所以我们下次反方向扩展 set2 return bfsHelper(set2, set, wordSet); }
当然,也可以不用递归,可以用两个队列就行了,直接把 这里 的代码贴过来供参考把,思想还是不变的。
public class Solution { public int ladderLength(String beginWord, String endWord, List<String> wordList) { if(!wordList.contains(endWord)) return 0; Set<String> beginSet = new HashSet<String>(), endSet = new HashSet<String>(); int len = 1; HashSet<String> visited = new HashSet<String>(); HashSet<String> dict = new HashSet<String>(wordList); beginSet.add(beginWord); endSet.add(endWord); while (!beginSet.isEmpty() && !endSet.isEmpty()) { if (beginSet.size() > endSet.size()) { Set<String> set = beginSet; beginSet = endSet; endSet = set; } Set<String> temp = new HashSet<String>(); for (String word : beginSet) { char[] chs = word.toCharArray(); for (int i = 0; i < chs.length; i++) { for (char c = 'a'; c <= 'z'; c++) { char old = chs[i]; chs[i] = c; String target = String.valueOf(chs); if (endSet.contains(target)) { return len + 1; } if (!visited.contains(target) && dict.contains(target)) { temp.add(target); visited.add(target); } chs[i] = old; } } } beginSet = temp; len++; } return 0; } }
总
基本上和 126 题 解决思路是一样的,主要就是 BFS
的应用。解法二中,直接在递归中的基础上用全局变量,有时候确实很方便,哈哈,比如之前的 124 题。