leetcode
leetcode 101 ~ 150
单词接龙

单词接龙

难度:

标签:

题目描述

字典 wordList 中从单词 beginWord endWord转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  •  对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
  • sk == endWord

给你两个单词 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 中的所有字符串 互不相同

代码结果

运行时间: 136 ms, 内存: 30.0 MB


/*
  * 思路:
  * 1. 使用BFS和Java Stream处理数据。
  * 2. 将wordList转换为Set以便快速查找。
  * 3. 使用队列进行层级遍历,逐层检查可能的单词。
  * 4. 使用流式处理生成可能的新单词,并过滤已访问的单词。
  * 5. 如果找到endWord,返回层级;否则返回0。
  */
import java.util.*;
import java.util.stream.*;
 
public class SolutionStream {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> wordSet = new HashSet<>(wordList);
        if (!wordSet.contains(endWord)) return 0;
 
        Queue<String> queue = new LinkedList<>();
        queue.offer(beginWord);
        int level = 1;
 
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String word = queue.poll();
                char[] chars = word.toCharArray();
                for (int j = 0; j < chars.length; j++) {
                    char originalChar = chars[j];
                    chars[j] = '*';
                    String pattern = new String(chars);
                    List<String> newWords = wordSet.stream()
                        .filter(w -> matches(pattern, w))
                        .collect(Collectors.toList());
                    for (String newWord : newWords) {
                        if (newWord.equals(endWord)) return level + 1;
                        queue.offer(newWord);
                        wordSet.remove(newWord);
                    }
                    chars[j] = originalChar;
                }
            }
            level++;
        }
 
        return 0;
    }
 
    private boolean matches(String pattern, String word) {
        if (pattern.length() != word.length()) return false;
        for (int i = 0; i < pattern.length(); i++) {
            if (pattern.charAt(i) != '*' && pattern.charAt(i) != word.charAt(i))
                return false;
        }
        return true;
    }
}

解释

方法:

这个题解采用了双向广度优先搜索(BFS)的思路来解决单词接龙问题。首先,将所有单词存储在一个字典中,并为每个单词分配一个唯一的ID。然后,构建一个无向图,其中每个节点表示一个单词,如果两个单词只相差一个字母,则在它们之间添加一条边。接下来,同时从起点单词和终点单词开始进行BFS,每次搜索一层节点。如果在某一层发现起点单词的BFS和终点单词的BFS相遇,则找到了最短转换序列,返回序列的长度。如果其中一个BFS搜索完了所有节点而没有相遇,则说明不存在转换序列,返回0。

时间复杂度:

O(N * L^2)

空间复杂度:

O(N * L)

代码细节讲解

🦆
为什么选择双向广度优先搜索(BFS)而不是单向BFS来解决这个问题?
双向广度优先搜索(BFS)通常比单向BFS更有效,因为它从两个方向同时搜索路径,一旦两个搜索在中间某点相遇,即完成搜索。这样可以大大减少搜索空间。在单词接龙问题中,起点和终点已知,利用双向BFS可以从两端同时逼近对方,当两个搜索相遇时,即找到最短路径。这种方法在最坏情况下的时间复杂度通常优于单向BFS。
🦆
在构建无向图时,为什么每个单词字符都替换成通配符'*',这种做法有什么好处?
将每个单词字符替换成通配符'*'可以有效地将所有可能通过单一字符变化能相互转换的单词组合起来。这样做可以创建一个虚拟节点,该节点连接所有只有一个字母不同的单词。例如,单词'log'和'cog'都可以通过虚拟节点'*og'相连。这种方法简化了图的构建过程,使得每个单词与其可能的变种之间的关系更加明确,从而加快了搜索速度,尤其是在单词量大的情况下。
🦆
题解中提到如果endWord不在wordId中,就返回0。这是否意味着endWord必须在wordList中才可能有解?
是的,endWord必须在wordList中才可能有解。在这种实现中,所有的单词都是从wordList加入图中的,如果endWord不在wordList中,则没有将其加入图中的步骤。因此,如果endWord不在wordList或者不是初始的beginWord,那么在图中找不到对应的节点,也就无法进行有效的路径搜索。这意味着找不到从beginWord到endWord的有效转换序列。
🦆
双向BFS中,如何确保两个搜索方向在恰当的位置相遇?有什么机制来判定两个搜索已经相遇?
在双向BFS中,两个搜索队列分别从起点和终点出发。每进行一轮搜索时,都会检查当前扩展的节点是否已被对方搜索过。这是通过维护两个距离数组(disBegin和disEnd)来实现的,分别记录从起点和终点到每个节点的距离。每次节点扩展时,会检查该节点是否已在对方的距离数组中有记录(即距离不是无穷大)。如果条件成立,说明两边的搜索已经在这个节点相遇,从而可以计算出最短路径长度。这种机制确保了两个搜索方向在恰当的位置相遇,并能够立即识别出相遇点。

相关问题

单词接龙 II

按字典 wordList 完成从单词 beginWord 到单词 endWord 转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk 这样的单词序列,并满足:

  • 每对相邻的单词之间仅有单个字母不同。
  • 转换过程中的每个单词 si1 <= i <= k)必须是字典 wordList 中的单词。注意,beginWord 不必是字典 wordList 中的单词。
  • sk == endWord

给你两个单词 beginWordendWord ,以及一个字典 wordList 。请你找出并返回所有从 beginWordendWord最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk] 的形式返回。

 

示例 1:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:[["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]]
解释:存在 2 种最短的转换序列:
"hit" -> "hot" -> "dot" -> "dog" -> "cog"
"hit" -> "hot" -> "lot" -> "log" -> "cog"

示例 2:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
输出:[]
解释:endWord "cog" 不在字典 wordList 中,所以不存在符合要求的转换序列。

 

提示:

  • 1 <= beginWord.length <= 5
  • endWord.length == beginWord.length
  • 1 <= wordList.length <= 500
  • wordList[i].length == beginWord.length
  • beginWordendWordwordList[i] 由小写英文字母组成
  • beginWord != endWord
  • wordList 中的所有单词 互不相同

最小基因变化

基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A''C''G''T' 之一。

假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。

  • 例如,"AACCGGTT" --> "AACCGGTA" 就是一次基因变化。

另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank 中)

给你两个基因序列 startend ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1

注意:起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。

 

示例 1:

输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]
输出:1

示例 2:

输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]
输出:2

示例 3:

输入:start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"]
输出:3

 

提示:

  • start.length == 8
  • end.length == 8
  • 0 <= bank.length <= 10
  • bank[i].length == 8
  • startendbank[i] 仅由字符 ['A', 'C', 'G', 'T'] 组成