题目描述(中等难度)

127. Word Ladder - 图1

给定一个开始单词和一个结束单词,一个单词列表,两个单词间转换原则是有且仅有一个字母不同。求出从开始单词转换到结束单词的最短路径长度是多少。

思路分析

基本上就是 126 题 的简化版了,可以先看一下 126 题 的解法思路。接下来就按照 126 题的思路讲了。把之前的图贴过来。

127. Word Ladder - 图2

要求最短的路径,DFS 肯定在这里就不合适了。而在之前我们用 BFS 将每个节点的邻接节点求了出来,这个过程其实我们相当于已经把最短路径的长度求出来了。所以我们只需要把之前的 BFS 拿过来稍加修改即可。

解法一 BFS

添加一个变量 len,遍历完每一层就将 len1

  1. public int ladderLength(String beginWord, String endWord, List<String> wordList) {
  2. if (!wordList.contains(endWord)) {
  3. return 0;
  4. }
  5. int len = 0;
  6. Queue<String> queue = new LinkedList<>();
  7. queue.offer(beginWord);
  8. boolean isFound = false;
  9. Set<String> dict = new HashSet<>(wordList);
  10. Set<String> visited = new HashSet<>();
  11. visited.add(beginWord);
  12. while (!queue.isEmpty()) {
  13. int size = queue.size();
  14. Set<String> subVisited = new HashSet<>();
  15. for (int j = 0; j < size; j++) {
  16. String temp = queue.poll();
  17. // 一次性得到所有的下一个的节点
  18. ArrayList<String> neighbors = getNeighbors(temp, dict);
  19. for (String neighbor : neighbors) {
  20. if (!visited.contains(neighbor)) {
  21. subVisited.add(neighbor);
  22. //到达了结束单词,提前结束
  23. if (neighbor.equals(endWord)) {
  24. isFound = true;
  25. break;
  26. }
  27. queue.offer(neighbor);
  28. }
  29. }
  30. }
  31. //当前层添加了元素,长度加一
  32. if (subVisited.size() > 0) {
  33. len++;
  34. }
  35. visited.addAll(subVisited);
  36. //找到以后,提前结束 while 循环,并且因为这里的层数从 0 计数,所以还需要加 1
  37. if (isFound) {
  38. len++;
  39. break;
  40. }
  41. }
  42. return len;
  43. }
  44. private ArrayList<String> getNeighbors(String node, Set<String> dict) {
  45. ArrayList<String> res = new ArrayList<String>();
  46. char chs[] = node.toCharArray();
  47. for (char ch = 'a'; ch <= 'z'; ch++) {
  48. for (int i = 0; i < chs.length; i++) {
  49. if (chs[i] == ch)
  50. continue;
  51. char old_ch = chs[i];
  52. chs[i] = ch;
  53. if (dict.contains(String.valueOf(chs))) {
  54. res.add(String.valueOf(chs));
  55. }
  56. chs[i] = old_ch;
  57. }
  58. }
  59. return res;
  60. }

126 题 中介绍了得到当前节点的相邻节点的两种方案,官方题解 中又提供了一种思路,虽然不容易想到,但蛮有意思,分享一下。

就是把所有的单词归类,具体的例子会好理解一些。

  1. 一个单词会产生三个类别,比如 hot 会产生
  2. *ot
  3. h*t
  4. ho*
  5. 然后考虑每一个单词,如果产生了相同的类,就把这些单词放在一起
  6. 假如我们的单词列表是 ["hot","dot","dog","lot","log","cog"]
  7. 考虑 hot,当前的分类结果如下
  8. *ot -> [hot]
  9. h*t -> [hot]
  10. ho* -> [hot]
  11. 再考虑 dot,当前的分类结果如下
  12. *ot -> [hot dot]
  13. h*t -> [hot]
  14. ho* -> [hot]
  15. d*t -> [dot]
  16. do* -> [dot]
  17. 再考虑 dog,当前的分类结果如下
  18. *ot -> [hot dot]
  19. h*t -> [hot]
  20. ho* -> [hot]
  21. d*t -> [dot]
  22. do* -> [dot dog]
  23. *og -> [dog]
  24. d*g -> [dog]
  25. 再考虑 lot,当前的分类结果如下
  26. *ot -> [hot dot lot]
  27. h*t -> [hot]
  28. ho* -> [hot]
  29. d*t -> [dot]
  30. do* -> [dot dog]
  31. *og -> [dog]
  32. d*g -> [dog]
  33. l*t -> [lot]
  34. lo* -> [lot]
  35. 然后把每个单词都放到对应的类中,这样最后找当前单词邻居节点的时候就方便了。
  36. 比如找 hot 的邻居节点,因为它可以产生 *ot h*t ho* 三个类别,所有它的相邻节点就是上边分好类的相应单词

解法二 双向搜索

126 题 最后一种解法中介绍了双向搜索,大大降低了时间复杂度。当然这里也可以直接用,同样是增加 len 变量即可,只不过之前用的递归,把 len 加到全局变量会更加方便些。

  1. int len = 2; //因为把 beginWord 和 endWord 都加入了路径,所以初始化 2
  2. public int ladderLength(String beginWord, String endWord, List<String> wordList) {
  3. if (!wordList.contains(endWord)) {
  4. return 0;
  5. }
  6. // 利用 BFS 得到所有的邻居节点
  7. Set<String> set1 = new HashSet<String>();
  8. set1.add(beginWord);
  9. Set<String> set2 = new HashSet<String>();
  10. set2.add(endWord);
  11. Set<String> wordSet = new HashSet<String>(wordList);
  12. //最后没找到返回 0
  13. if (!bfsHelper(set1, set2, wordSet)) {
  14. return 0;
  15. }
  16. return len;
  17. }
  18. private boolean bfsHelper(Set<String> set1, Set<String> set2, Set<String> wordSet) {
  19. if (set1.isEmpty()) {
  20. return false;
  21. }
  22. // set1 的数量多,就反向扩展
  23. if (set1.size() > set2.size()) {
  24. return bfsHelper(set2, set1, wordSet);
  25. }
  26. // 将已经访问过单词删除
  27. wordSet.removeAll(set1);
  28. wordSet.removeAll(set2);
  29. // 保存新扩展得到的节点
  30. Set<String> set = new HashSet<String>();
  31. for (String str : set1) {
  32. // 遍历每一位
  33. for (int i = 0; i < str.length(); i++) {
  34. char[] chars = str.toCharArray();
  35. // 尝试所有字母
  36. for (char ch = 'a'; ch <= 'z'; ch++) {
  37. if (chars[i] == ch) {
  38. continue;
  39. }
  40. chars[i] = ch;
  41. String word = new String(chars);
  42. if (set2.contains(word)) {
  43. return true;
  44. }
  45. // 如果还没有相遇,并且新的单词在 word 中,那么就加到 set 中
  46. if (wordSet.contains(word)) {
  47. set.add(word);
  48. }
  49. }
  50. }
  51. }
  52. //如果当前进行了扩展,长度加 1
  53. if (set.size() > 0) {
  54. len++;
  55. }
  56. // 一般情况下新扩展的元素会多一些,所以我们下次反方向扩展 set2
  57. return bfsHelper(set2, set, wordSet);
  58. }

当然,也可以不用递归,可以用两个队列就行了,直接把 这里 的代码贴过来供参考把,思想还是不变的。

  1. public class Solution {
  2. public int ladderLength(String beginWord, String endWord, Set<String> wordList) {
  3. Set<String> beginSet = new HashSet<String>(), endSet = new HashSet<String>();
  4. int len = 1;
  5. int strLen = beginWord.length();
  6. HashSet<String> visited = new HashSet<String>();
  7. beginSet.add(beginWord);
  8. endSet.add(endWord);
  9. while (!beginSet.isEmpty() && !endSet.isEmpty()) {
  10. if (beginSet.size() > endSet.size()) {
  11. Set<String> set = beginSet;
  12. beginSet = endSet;
  13. endSet = set;
  14. }
  15. Set<String> temp = new HashSet<String>();
  16. for (String word : beginSet) {
  17. char[] chs = word.toCharArray();
  18. for (int i = 0; i < chs.length; i++) {
  19. for (char c = 'a'; c <= 'z'; c++) {
  20. char old = chs[i];
  21. chs[i] = c;
  22. String target = String.valueOf(chs);
  23. if (endSet.contains(target)) {
  24. return len + 1;
  25. }
  26. if (!visited.contains(target) && wordList.contains(target)) {
  27. temp.add(target);
  28. visited.add(target);
  29. }
  30. chs[i] = old;
  31. }
  32. }
  33. }
  34. beginSet = temp;
  35. len++;
  36. }
  37. return 0;
  38. }
  39. }

基本上和 126 题 解决思路是一样的,主要就是 BFS 的应用。解法二中,直接在递归中的基础上用全局变量,有时候确实很方便,哈哈,比如之前的 124 题

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情