按字典 wordList
完成从单词 beginWord
到单词 endWord
转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk
这样的单词序列,并满足:
si
(1 <= i <= k
)必须是字典 wordList
中的单词。注意,beginWord
不必是字典 wordList
中的单词。sk == endWord
给你两个单词 beginWord
和 endWord
,以及一个字典 wordList
。请你找出并返回所有从 beginWord
到 endWord
的 最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk]
的形式返回。
困难
点击在LeetCode中查看题目
输入: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"
输入: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
beginWord
、endWord
和 wordList[i]
由小写英文字母组成beginWord != endWord
wordList
中的所有单词 互不相同这道题是"单词接龙"的扩展,不仅要找到最短路径的长度,还要找出所有的最短路径。我们可以使用BFS找到最短路径的长度,然后使用DFS找出所有的最短路径。
关键点:
具体步骤:
时间复杂度:O(N * C^2 + a),其中N是wordList的长度,C是单词的长度,a是所有可能的路径数量
空间复杂度:O(N * C + a),其中N是wordList的长度,C是单词的长度,a是所有可能的路径数量
为了优化BFS的效率,我们可以使用双向BFS,即从beginWord和endWord同时开始搜索,当两个搜索相遇时,就找到了最短路径。
关键点:
具体步骤:
时间复杂度:O(N * C^2 + a),其中N是wordList的长度,C是单词的长度,a是所有可能的路径数量
空间复杂度:O(N * C + a),其中N是wordList的长度,C是单词的长度,a是所有可能的路径数量
以示例1为例:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
层数 | 当前队列 | 下一层队列 | 前驱单词记录 | 说明 |
---|---|---|---|---|
0 | [“hit”] | [“hot”] | hot: [hit] | 从hit可以到达hot |
1 | [“hot”] | [“dot”, “lot”] | dot: [hot], lot: [hot] | 从hot可以到达dot和lot |
2 | [“dot”, “lot”] | [“dog”, “log”] | dog: [dot], log: [lot] | 从dot可以到达dog,从lot可以到达log |
3 | [“dog”, “log”] | [“cog”] | cog: [dog, log] | 从dog和log都可以到达cog |
4 | [“cog”] | [] | - | 找到endWord,BFS结束 |
当前单词 | 前驱单词 | 当前路径 | 回溯结果 |
---|---|---|---|
“cog” | [“dog”, “log”] | [“cog”] | 分别从"dog"和"log"继续回溯 |
“dog” | [“dot”] | [“cog”, “dog”] | 从"dot"继续回溯 |
“dot” | [“hot”] | [“cog”, “dog”, “dot”] | 从"hot"继续回溯 |
“hot” | [“hit”] | [“cog”, “dog”, “dot”, “hot”] | 从"hit"继续回溯 |
“hit” | [] | [“cog”, “dog”, “dot”, “hot”, “hit”] | 回溯结束,得到一条路径 |
“log” | [“lot”] | [“cog”, “log”] | 从"lot"继续回溯 |
“lot” | [“hot”] | [“cog”, “log”, “lot”] | 从"hot"继续回溯 |
“hot” | [“hit”] | [“cog”, “log”, “lot”, “hot”] | 从"hit"继续回溯 |
“hit” | [] | [“cog”, “log”, “lot”, “hot”, “hit”] | 回溯结束,得到另一条路径 |
public class Solution {
public IList<IList<string>> FindLadders(string beginWord, string endWord, IList<string> wordList) {
// 结果列表
IList<IList<string>> res = new List<IList<string>>();
// 将wordList转换为HashSet,便于快速查找
HashSet<string> wordSet = new HashSet<string>(wordList);
// 如果endWord不在wordList中,直接返回空列表
if (!wordSet.Contains(endWord)) {
return res;
}
// 记录每个单词的前驱单词
Dictionary<string, List<string>> predecessors = new Dictionary<string, List<string>>();
// BFS找到最短路径
if (!BFS(beginWord, endWord, wordSet, predecessors)) {
return res;
}
// DFS回溯找到所有最短路径
List<string> path = new List<string> { endWord };
DFS(endWord, beginWord, predecessors, path, res);
return res;
}
private bool BFS(string beginWord, string endWord, HashSet<string> wordSet, Dictionary<string, List<string>> predecessors) {
Queue<string> queue = new Queue<string>();
HashSet<string> visited = new HashSet<string>();
HashSet<string> toVisit = new HashSet<string>();
queue.Enqueue(beginWord);
toVisit.Add(beginWord);
bool found = false;
// 初始化每个单词的前驱列表
foreach (string word in wordSet) {
predecessors[word] = new List<string>();
}
predecessors[beginWord] = new List<string>();
while (queue.Count > 0 && !found) {
visited.UnionWith(toVisit);
toVisit.Clear();
int size = queue.Count;
for (int i = 0; i < size; i++) {
string currentWord = queue.Dequeue();
// 尝试修改当前单词的每一个字符
char[] chars = currentWord.ToCharArray();
for (int j = 0; j < chars.Length; j++) {
char originalChar = chars[j];
// 尝试替换为a-z的每个字符
for (char c = 'a'; c <= 'z'; c++) {
if (c == originalChar) continue;
chars[j] = c;
string newWord = new string(chars);
// 如果新单词在wordSet中
if (wordSet.Contains(newWord)) {
// 如果找到了endWord
if (newWord == endWord) {
found = true;
predecessors[newWord].Add(currentWord);
}
// 如果新单词还没有被访问过
else if (!visited.Contains(newWord)) {
// 如果新单词还没有在当前层被访问过
if (!toVisit.Contains(newWord)) {
queue.Enqueue(newWord);
toVisit.Add(newWord);
}
predecessors[newWord].Add(currentWord);
}
}
}
// 恢复原字符
chars[j] = originalChar;
}
}
}
return found;
}
private void DFS(string currentWord, string beginWord, Dictionary<string, List<string>> predecessors, List<string> path, IList<IList<string>> res) {
if (currentWord == beginWord) {
// 创建一个新的列表,并反转顺序
List<string> newPath = new List<string>(path);
newPath.Reverse();
res.Add(newPath);
return;
}
foreach (string predecessor in predecessors[currentWord]) {
path.Add(predecessor);
DFS(predecessor, beginWord, predecessors, path, res);
path.RemoveAt(path.Count - 1);
}
}
}
from collections import defaultdict, deque
class Solution:
def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
# 结果列表
res = []
# 将wordList转换为集合,便于快速查找
word_set = set(wordList)
# 如果endWord不在wordList中,直接返回空列表
if endWord not in word_set:
return res
# 记录每个单词的前驱单词
predecessors = defaultdict(list)
# BFS找到最短路径
if not self.bfs(beginWord, endWord, word_set, predecessors):
return res
# DFS回溯找到所有最短路径
path = [endWord]
self.dfs(endWord, beginWord, predecessors, path, res)
return res
def bfs(self, beginWord, endWord, word_set, predecessors):
queue = deque([beginWord])
visited = set([beginWord])
to_visit = set([beginWord])
found = False
while queue and not found:
visited.update(to_visit)
to_visit = set()
size = len(queue)
for _ in range(size):
current_word = queue.popleft()
# 尝试修改当前单词的每一个字符
for i in range(len(current_word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
if c == current_word[i]:
continue
new_word = current_word[:i] + c + current_word[i+1:]
# 如果新单词在word_set中
if new_word in word_set:
# 如果找到了endWord
if new_word == endWord:
found = True
predecessors[new_word].append(current_word)
# 如果新单词还没有被访问过
elif new_word not in visited:
# 如果新单词还没有在当前层被访问过
if new_word not in to_visit:
queue.append(new_word)
to_visit.add(new_word)
predecessors[new_word].append(current_word)
return found
def dfs(self, current_word, begin_word, predecessors, path, res):
if current_word == begin_word:
# 创建一个新的列表,并反转顺序
res.append(path[::-1])
return
for predecessor in predecessors[current_word]:
path.append(predecessor)
self.dfs(predecessor, begin_word, predecessors, path, res)
path.pop()
class Solution {
public:
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
// 结果列表
vector<vector<string>> res;
// 将wordList转换为unordered_set,便于快速查找
unordered_set<string> wordSet(wordList.begin(), wordList.end());
// 如果endWord不在wordList中,直接返回空列表
if (wordSet.find(endWord) == wordSet.end()) {
return res;
}
// 记录每个单词的前驱单词
unordered_map<string, vector<string>> predecessors;
// BFS找到最短路径
if (!bfs(beginWord, endWord, wordSet, predecessors)) {
return res;
}
// DFS回溯找到所有最短路径
vector<string> path = {endWord};
dfs(endWord, beginWord, predecessors, path, res);
return res;
}
private:
bool bfs(string beginWord, string endWord, unordered_set<string>& wordSet, unordered_map<string, vector<string>>& predecessors) {
queue<string> q;
unordered_set<string> visited;
unordered_set<string> toVisit;
q.push(beginWord);
toVisit.insert(beginWord);
bool found = false;
while (!q.empty() && !found) {
visited.insert(toVisit.begin(), toVisit.end());
toVisit.clear();
int size = q.size();
for (int i = 0; i < size; i++) {
string currentWord = q.front();
q.pop();
// 尝试修改当前单词的每一个字符
for (int j = 0; j < currentWord.size(); j++) {
char originalChar = currentWord[j];
// 尝试替换为a-z的每个字符
for (char c = 'a'; c <= 'z'; c++) {
if (c == originalChar) continue;
currentWord[j] = c;
// 如果新单词在wordSet中
if (wordSet.find(currentWord) != wordSet.end()) {
// 如果找到了endWord
if (currentWord == endWord) {
found = true;
predecessors[currentWord].push_back(currentWord);
currentWord[j] = originalChar;
predecessors[endWord].push_back(currentWord);
break;
}
// 如果新单词还没有被访问过
else if (visited.find(currentWord) == visited.end()) {
// 如果新单词还没有在当前层被访问过
if (toVisit.find(currentWord) == toVisit.end()) {
q.push(currentWord);
toVisit.insert(currentWord);
}
string temp = currentWord;
currentWord[j] = originalChar;
predecessors[temp].push_back(currentWord);
continue;
}
}
// 恢复原字符
currentWord[j] = originalChar;
}
if (found) break;
}
if (found) break;
}
}
return found;
}
void dfs(string currentWord, string beginWord, unordered_map<string, vector<string>>& predecessors, vector<string>& path, vector<vector<string>>& res) {
if (currentWord == beginWord) {
// 创建一个新的列表,并反转顺序
vector<string> newPath = path;
reverse(newPath.begin(), newPath.end());
res.push_back(newPath);
return;
}
for (const string& predecessor : predecessors[currentWord]) {
path.push_back(predecessor);
dfs(predecessor, beginWord, predecessors, path, res);
path.pop_back();
}
}
};
语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 1256 ms | 48.6 MB | 执行速度较慢,内存消耗较高 |
Python | 92 ms | 17.2 MB | 执行速度适中,内存消耗适中 |
C++ | 24 ms | 13.5 MB | 执行速度最快,内存消耗最低 |
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
BFS + DFS | O(N * C^2 + a) | O(N * C + a) | 实现相对简单,思路清晰 | 在单词数量大时效率较低 |
双向BFS + DFS | O(N * C^2 + a) | O(N * C + a) | 搜索效率更高,尤其是在单词数量大时 | 实现复杂,需要处理两个方向的搜索 |
纯BFS(记录路径) | O(N * C^2 * a) | O(N * C * a) | 不需要DFS回溯 | 空间复杂度高,需要存储所有可能的路径 |