描述

字典 wordList 中从单词 beginWordendWord转换序列 是一个按下述规格形成的序列:

  • 序列中第一个单词是 beginWord
  • 序列中最后一个单词是 endWord
  • 每次转换只能改变一个字母。
  • 转换过程中的中间单词必须是字典 wordList 中的单词。

给你两个单词 beginWordendWord和一个字典 wordList ,找到从 beginWordendWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0

示例

示例 1:

  1. 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
  2. 输出:5
  3. 解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5

示例 2:

  1. 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
  2. 输出:0
  3. 解释:endWord "cog" 不在字典中,所以无法进行转换。

提示

  • 1 <= beginWord.length <= 10
  • endWord.length == beginWord.length
  • 1 <= wordList.length <= 5000
  • wordList[i].length == beginWord.length
  • beginWordendWordwordList[i] 由小写英文字母组成
  • beginWord != endWord
  • wordList 中的所有字符串 互不相同

解题思路

建图 + 广度优先遍历
单词接龙
广度优先遍历、双向广度优先遍历(Java)

方法一:广度优先遍历

我们可以把每个单词都抽象为一个点,如果两个单词可以只改变一个字母进行转换,那么说明他们之间有一条双向边。因此我们只需要把满足转换条件的点相连,就形成了一张

基于该图,我们以 beginWord 为图的起点,以 endWord 为终点进行广度优先搜索,寻找 beginWordendWord 的最短路径。
1.png

算法

我们在遍历一开始,把所有的单词列表放进一个哈希表 wordSet 中,然后在遍历的时候构建图,每一次得到在单词列表里可以转换的单词.

使用 BFS 进行遍历,需要的辅助数据结构是:

  • 队列;
  • visited 集合。说明:可以直接在 wordSet (由 wordList 放进集合中得到)里做删除。但更好的做法是新开一个哈希表,遍历过的字符串放进哈希表里。这种做法具有普遍意义。绝大多数在线测评系统和应用场景都不会在意空间开销。

代码

  1. import java.util.ArrayList;
  2. import java.util.Collections;
  3. import java.util.HashSet;
  4. import java.util.LinkedList;
  5. import java.util.List;
  6. import java.util.Queue;
  7. import java.util.Set;
  8. public class Solution {
  9. public int ladderLength(String beginWord, String endWord, List<String> wordList) {
  10. // 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
  11. Set<String> wordSet = new HashSet<>(wordList);
  12. if (wordSet.size() == 0 || !wordSet.contains(endWord)) {
  13. return 0;
  14. }
  15. wordSet.remove(beginWord);
  16. // 第 2 步:图的广度优先遍历,必须使用队列和表示是否访问过的 visited 哈希表
  17. Queue<String> queue = new LinkedList<>();
  18. queue.offer(beginWord);
  19. Set<String> visited = new HashSet<>();
  20. visited.add(beginWord);
  21. // 第 3 步:开始广度优先遍历,包含起点,因此初始化的时候步数为 1
  22. int step = 1;
  23. while (!queue.isEmpty()) {
  24. int currentSize = queue.size();
  25. for (int i = 0; i < currentSize; i++) {
  26. // 依次遍历当前队列中的单词
  27. String currentWord = queue.poll();
  28. // 如果 currentWord 能够修改 1 个字符与 endWord 相同,则返回 step + 1
  29. if (changeWordEveryOneLetter(currentWord, endWord, queue, visited, wordSet)) {
  30. return step + 1;
  31. }
  32. }
  33. step++;
  34. }
  35. return 0;
  36. }
  37. /**
  38. * 尝试对 currentWord 修改每一个字符,看看是不是能与 endWord 匹配
  39. *
  40. * @param currentWord
  41. * @param endWord
  42. * @param queue
  43. * @param visited
  44. * @param wordSet
  45. * @return
  46. */
  47. private boolean changeWordEveryOneLetter(String currentWord, String endWord,
  48. Queue<String> queue, Set<String> visited, Set<String> wordSet) {
  49. char[] charArray = currentWord.toCharArray();
  50. for (int i = 0; i < endWord.length(); i++) {
  51. // 先保存,然后恢复
  52. char originChar = charArray[i];
  53. for (char k = 'a'; k <= 'z'; k++) {
  54. if (k == originChar) {
  55. continue;
  56. }
  57. charArray[i] = k;
  58. String nextWord = String.valueOf(charArray);
  59. if (wordSet.contains(nextWord)) {
  60. if (nextWord.equals(endWord)) {
  61. return true;
  62. }
  63. if (!visited.contains(nextWord)) {
  64. queue.add(nextWord);
  65. // 注意:添加到队列以后,必须马上标记为已经访问
  66. visited.add(nextWord);
  67. }
  68. }
  69. }
  70. // 恢复
  71. charArray[i] = originChar;
  72. }
  73. return false;
  74. }
  75. }

方法二:双向广度优先遍历

思路及解法

根据给定字典构造的图可能会很大,而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。假如每个节点的分支数量相同,搜索空间会随着层数的增长指数级的增加。考虑一个简单的二叉树,每一层都是满二叉树的扩展,节点的数量会以 2 为底数呈指数增长。

如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord 开始,另一边从 endWord 开始。我们每次从两边各扩展一层节点,当发现某一时刻两边都访问过同一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而提高代码运行效率。

2.png

  • 已知目标顶点的情况下,可以分别从起点和目标顶点(终点)执行广度优先遍历,直到遍历的部分有交集。这种方式搜索的单词数量会更小一些;
  • 更合理的做法是,每次从单词数量小的集合开始扩散
  • 这里 beginVisitedendVisited 交替使用,等价于单向 BFS 里使用队列,每次扩散都要加到总的 visited 里。

代码

  1. import java.util.ArrayList;
  2. import java.util.Collections;
  3. import java.util.HashSet;
  4. import java.util.List;
  5. import java.util.Set;
  6. public class Solution {
  7. public int ladderLength(String beginWord, String endWord, List<String> wordList) {
  8. // 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
  9. Set<String> wordSet = new HashSet<>(wordList);
  10. if (wordSet.size() == 0 || !wordSet.contains(endWord)) {
  11. return 0;
  12. }
  13. // 第 2 步:已经访问过的 word 添加到 visited 哈希表里
  14. Set<String> visited = new HashSet<>();
  15. // 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用
  16. Set<String> beginVisited = new HashSet<>();
  17. beginVisited.add(beginWord);
  18. Set<String> endVisited = new HashSet<>();
  19. endVisited.add(endWord);
  20. // 第 3 步:执行双向 BFS,左右交替扩散的步数之和为所求
  21. int step = 1;
  22. while (!beginVisited.isEmpty() && !endVisited.isEmpty()) {
  23. // 优先选择小的哈希表进行扩散,考虑到的情况更少
  24. if (beginVisited.size() > endVisited.size()) {
  25. Set<String> temp = beginVisited;
  26. beginVisited = endVisited;
  27. endVisited = temp;
  28. }
  29. // 逻辑到这里,保证 beginVisited 是相对较小的集合,nextLevelVisited 在扩散完成以后,会成为新的 beginVisited
  30. Set<String> nextLevelVisited = new HashSet<>();
  31. for (String word : beginVisited) {
  32. if (changeWordEveryOneLetter(word, endVisited, visited, wordSet, nextLevelVisited)) {
  33. return step + 1;
  34. }
  35. }
  36. // 原来的 beginVisited 废弃,从 nextLevelVisited 开始新的双向 BFS
  37. beginVisited = nextLevelVisited;
  38. step++;
  39. }
  40. return 0;
  41. }
  42. /**
  43. * 尝试对 word 修改每一个字符,看看是不是能落在 endVisited 中,扩展得到的新的 word 添加到 nextLevelVisited 里
  44. *
  45. * @param word
  46. * @param endVisited
  47. * @param visited
  48. * @param wordSet
  49. * @param nextLevelVisited
  50. * @return
  51. */
  52. private boolean changeWordEveryOneLetter(String word, Set<String> endVisited,
  53. Set<String> visited,
  54. Set<String> wordSet,
  55. Set<String> nextLevelVisited) {
  56. char[] charArray = word.toCharArray();
  57. for (int i = 0; i < word.length(); i++) {
  58. char originChar = charArray[i];
  59. for (char c = 'a'; c <= 'z'; c++) {
  60. if (originChar == c) {
  61. continue;
  62. }
  63. charArray[i] = c;
  64. String nextWord = String.valueOf(charArray);
  65. if (wordSet.contains(nextWord)) {
  66. if (endVisited.contains(nextWord)) {
  67. return true;
  68. }
  69. if (!visited.contains(nextWord)) {
  70. nextLevelVisited.add(nextWord);
  71. visited.add(nextWord);
  72. }
  73. }
  74. }
  75. // 恢复,下次再用
  76. charArray[i] = originChar;
  77. }
  78. return false;
  79. }
  80. }