加载中...
127-单词接龙(Word Ladder)
发表于:2021-12-03 | 分类: 困难
字数统计: 471 | 阅读时长: 2分钟 | 阅读量:

原文链接: https://leetcode-cn.com/problems/word-ladder

英文原文

A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that:

  • Every adjacent pair of words differs by a single letter.
  • Every si for 1 <= i <= k is in wordList. Note that beginWord does not need to be in wordList.
  • sk == endWord

Given two words, beginWord and endWord, and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, or 0 if no such sequence exists.

 

Example 1:

Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
Output: 5
Explanation: One shortest transformation sequence is "hit" -> "hot" -> "dot" -> "dog" -> cog", which is 5 words long.

Example 2:

Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
Output: 0
Explanation: The endWord "cog" is not in wordList, therefore there is no valid transformation sequence.

 

Constraints:

  • 1 <= beginWord.length <= 10
  • endWord.length == beginWord.length
  • 1 <= wordList.length <= 5000
  • wordList[i].length == beginWord.length
  • beginWord, endWord, and wordList[i] consist of lowercase English letters.
  • beginWord != endWord
  • All the words in wordList are unique.

中文题目

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

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

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

 

示例 1:

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

示例 2:

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

 

提示:

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

通过代码

高赞题解

一句话题解

  • 无向图中两个顶点之间的最短路径的长度,可以通过广度优先遍历得到;
  • 为什么 BFS 得到的路径最短?可以把起点和终点所在的路径拉直来看,两点之间线段最短
  • 已知目标顶点的情况下,可以分别从起点和目标顶点(终点)执行广度优先遍历,直到遍历的部分有交集,这是双向广度优先遍历的思想。

(参考代码应评论区用户要求进行了修改,2020 年 9 月 4 日。)


视频解题

视频时间线:建议倍速观看

  • 读题、讲解注意事项:00:00 开始
  • 分析示例 1 并讲解如何建图、BFS、双向 BFS 思路:02:58 开始
  • 讲解 BFS 代码:11:41 开始
  • 讲解双向 BFS 代码:18:54 开始

...题:单词接龙(单双向 BFS).mp4


分析题意:

  • 「转换」意即:两个单词对应位置只有一个字符不同,例如 “hit” 与 “hot”,这种转换是可以逆向的,因此,根据题目给出的单词列表,可以构建出一个无向(无权)图;

image.png

  • 如果一开始就构建图,每一个单词都需要和除它以外的另外的单词进行比较,复杂度是 $O(N \rm{wordLen})$,这里 $N$ 是单词列表的长度;
  • 为此,我们在遍历一开始,把所有的单词列表放进一个哈希表中,然后在遍历的时候构建图,每一次得到在单词列表里可以转换的单词,复杂度是 $O(26 \times \rm{wordLen})$,借助哈希表,找到邻居与 $N$ 无关;
  • 使用 BFS 进行遍历,需要的辅助数据结构是:
    • 队列;
    • visited 集合。说明:可以直接在 wordSet (由 wordList 放进集合中得到)里做删除。但更好的做法是新开一个哈希表,遍历过的字符串放进哈希表里。这种做法具有普遍意义。绝大多数在线测评系统和应用场景都不会在意空间开销。

方法一:广度优先遍历

参考代码 1

[]
import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.Set; public class Solution { public int ladderLength(String beginWord, String endWord, List<String> wordList) { // 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里 Set<String> wordSet = new HashSet<>(wordList); if (wordSet.size() == 0 || !wordSet.contains(endWord)) { return 0; } wordSet.remove(beginWord); // 第 2 步:图的广度优先遍历,必须使用队列和表示是否访问过的 visited 哈希表 Queue<String> queue = new LinkedList<>(); queue.offer(beginWord); Set<String> visited = new HashSet<>(); visited.add(beginWord); // 第 3 步:开始广度优先遍历,包含起点,因此初始化的时候步数为 1 int step = 1; while (!queue.isEmpty()) { int currentSize = queue.size(); for (int i = 0; i < currentSize; i++) { // 依次遍历当前队列中的单词 String currentWord = queue.poll(); // 如果 currentWord 能够修改 1 个字符与 endWord 相同,则返回 step + 1 if (changeWordEveryOneLetter(currentWord, endWord, queue, visited, wordSet)) { return step + 1; } } step++; } return 0; } /** * 尝试对 currentWord 修改每一个字符,看看是不是能与 endWord 匹配 * * @param currentWord * @param endWord * @param queue * @param visited * @param wordSet * @return */ private boolean changeWordEveryOneLetter(String currentWord, String endWord, Queue<String> queue, Set<String> visited, Set<String> wordSet) { char[] charArray = currentWord.toCharArray(); for (int i = 0; i < endWord.length(); i++) { // 先保存,然后恢复 char originChar = charArray[i]; for (char k = 'a'; k <= 'z'; k++) { if (k == originChar) { continue; } charArray[i] = k; String nextWord = String.valueOf(charArray); if (wordSet.contains(nextWord)) { if (nextWord.equals(endWord)) { return true; } if (!visited.contains(nextWord)) { queue.add(nextWord); // 注意:添加到队列以后,必须马上标记为已经访问 visited.add(nextWord); } } } // 恢复 charArray[i] = originChar; } return false; } }
[]
from typing import List from collections import deque class Solution: def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: word_set = set(wordList) if len(word_set) == 0 or endWord not in word_set: return 0 if beginWord in word_set: word_set.remove(beginWord) queue = deque() queue.append(beginWord) visited = set(beginWord) word_len = len(beginWord) step = 1 while queue: current_size = len(queue) for i in range(current_size): word = queue.popleft() word_list = list(word) for j in range(word_len): origin_char = word_list[j] for k in range(26): word_list[j] = chr(ord('a') + k) next_word = ''.join(word_list) if next_word in word_set: if next_word == endWord: return step + 1 if next_word not in visited: queue.append(next_word) visited.add(next_word) word_list[j] = origin_char step += 1 return 0 if __name__ == '__main__': beginWord = "hit" endWord = "cog" wordList = ["hot", "dot", "dog", "lot", "log", "cog"] solution = Solution() res = solution.ladderLength(beginWord, endWord, wordList) print(res)

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

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

image.png

参考代码 2

[]
import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; public class Solution { public int ladderLength(String beginWord, String endWord, List<String> wordList) { // 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里 Set<String> wordSet = new HashSet<>(wordList); if (wordSet.size() == 0 || !wordSet.contains(endWord)) { return 0; } // 第 2 步:已经访问过的 word 添加到 visited 哈希表里 Set<String> visited = new HashSet<>(); // 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用 Set<String> beginVisited = new HashSet<>(); beginVisited.add(beginWord); Set<String> endVisited = new HashSet<>(); endVisited.add(endWord); // 第 3 步:执行双向 BFS,左右交替扩散的步数之和为所求 int step = 1; while (!beginVisited.isEmpty() && !endVisited.isEmpty()) { // 优先选择小的哈希表进行扩散,考虑到的情况更少 if (beginVisited.size() > endVisited.size()) { Set<String> temp = beginVisited; beginVisited = endVisited; endVisited = temp; } // 逻辑到这里,保证 beginVisited 是相对较小的集合,nextLevelVisited 在扩散完成以后,会成为新的 beginVisited Set<String> nextLevelVisited = new HashSet<>(); for (String word : beginVisited) { if (changeWordEveryOneLetter(word, endVisited, visited, wordSet, nextLevelVisited)) { return step + 1; } } // 原来的 beginVisited 废弃,从 nextLevelVisited 开始新的双向 BFS beginVisited = nextLevelVisited; step++; } return 0; } /** * 尝试对 word 修改每一个字符,看看是不是能落在 endVisited 中,扩展得到的新的 word 添加到 nextLevelVisited 里 * * @param word * @param endVisited * @param visited * @param wordSet * @param nextLevelVisited * @return */ private boolean changeWordEveryOneLetter(String word, Set<String> endVisited, Set<String> visited, Set<String> wordSet, Set<String> nextLevelVisited) { char[] charArray = word.toCharArray(); for (int i = 0; i < word.length(); i++) { char originChar = charArray[i]; for (char c = 'a'; c <= 'z'; c++) { if (originChar == c) { continue; } charArray[i] = c; String nextWord = String.valueOf(charArray); if (wordSet.contains(nextWord)) { if (endVisited.contains(nextWord)) { return true; } if (!visited.contains(nextWord)) { nextLevelVisited.add(nextWord); visited.add(nextWord); } } } // 恢复,下次再用 charArray[i] = originChar; } return false; } }
[]
from typing import List from collections import deque class Solution: def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: word_set = set(wordList) if len(word_set) == 0 or endWord not in word_set: return 0 if beginWord in word_set: word_set.remove(beginWord) visited = set() visited.add(beginWord) visited.add(endWord) begin_visited = set() begin_visited.add(beginWord) end_visited = set() end_visited.add(endWord) word_len = len(beginWord) step = 1 # 简化成 while begin_visited 亦可 while begin_visited and end_visited: # 打开帮助调试 # print(begin_visited) # print(end_visited) if len(begin_visited) > len(end_visited): begin_visited, end_visited = end_visited, begin_visited next_level_visited = set() for word in begin_visited: word_list = list(word) for j in range(word_len): origin_char = word_list[j] for k in range(26): word_list[j] = chr(ord('a') + k) next_word = ''.join(word_list) if next_word in word_set: if next_word in end_visited: return step + 1 if next_word not in visited: next_level_visited.add(next_word) visited.add(next_word) word_list[j] = origin_char begin_visited = next_level_visited step += 1 return 0 if __name__ == '__main__': beginWord = "hit" endWord = "cog" wordList = ["hot", "dot", "dog", "lot", "log", "cog"] solution = Solution() res = solution.ladderLength(beginWord, endWord, wordList) print(res)

统计信息

通过次数 提交次数 AC比率
129990 276189 47.1%

提交历史

提交时间 提交结果 执行时间 内存消耗 语言

相似题目

题目 难度
单词接龙 II 困难
最小基因变化 中等
上一篇:
126-单词接龙 II(Word Ladder II)
下一篇:
128-最长连续序列(Longest Consecutive Sequence)
本文目录
本文目录