目录
1,题目描述
英文描述
Given two words (beginWord and endWord), and a dictionary's word list, find all shortest transformation sequence(s) from beginWord to endWord, such that:
Only one letter can be changed at a time
Each transformed word must exist in the word list. Note that beginWord is not a transformed word.
Note:Return an empty list if there is no such transformation sequence.
All words have the same length.
All words contain only lowercase alphabetic characters.
You may assume no duplicates in the word list.
You may assume beginWord and endWord are non-empty and are not the same.
Example 1:Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]Output:
[
["hit","hot","dot","dog","cog"],
["hit","hot","lot","log","cog"]
]
Example 2:Input:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]Output: []
Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.
中文描述
给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换需遵循如下规则:
每次转换只能改变一个字母。
转换后得到的单词必须是字典中的单词。
说明:如果不存在这样的转换序列,返回一个空列表。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]输出:
[
["hit","hot","dot","dog","cog"],
["hit","hot","lot","log","cog"]
]
示例 2:输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]输出: []
解释: endWord "cog" 不在字典中,所以不存在符合要求的转换序列。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-ladder-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
2,解题思路
图片来自@windliang【详细通俗的思路分析,多解法】,代码参考自题解记录,并未找到链接来源。
如何表示单词之间的关系?
当单词A通过替换一个字母得到单词B,且单词B存在于字典中时,此次转换有效,可以想到用图来表示两者之间的关系,每个单词表示一个节点,两个单词间转换关系有效时,用线连接起来;
其次,由于每个用例中起点和终点是固定的,所以任意两个单词间只会有单向的关系,并不会反复的进行转换,所以可以用树来表示关系;
有了联通的树,就可以从起点到终点进行最短路径的遍历求解;
如何构建图?
首先,在树中寻找起点到终点的最短路径(且节点间距离为1),可以直观地联想到BFS,因为BFS每次可以扩展一层的节点,只要一层中出现终点,就可以得出在这一层的所有答案。
为了避免盲目地构建图(任意两点只要转换关系有效,就进行连接,此方法冗余信息过多),可以选择在BFS过程中构建树。算法如下:
- 取出队列中的一个字符串s1,将字符串中每一个字符依次用26个英文字母进行替换;
- 查找替换后的字符串s2是否存在于合法字符串集合dirc中(unordered_set<string> dirc(wordList.begin(), wordList.end())),存在即说明s1到s2转换关系有效;
双向BFS,简单来说就是从起点(自顶向下)和终点(自底向上)同时进行BFS:
- 每当一层节点扩展完毕后,比较两种方法的BFS队列中下一次需要扩展的节点数目,选择节点数目小的进行下一次扩展;
- 直到两种方法的BFS队列中出现相同的节点(说明整棵树已经打通,联通的路径即为起点到终点的最短路径),BFS过程结束;
- 【使用双向BFS的好处在于,每次扩展可以选择扩展节点数目最少的一个方向,这样可以有效地进行剪枝,使得目标状态(从起点到终点形成完整通路}尽可能早地出现】
具体实现技巧:
- 使用findFlag两队列是否存在相同的元素,reverseFlag时刻标记当前BFS遍历的方向(false为自顶向下,true为自底向上);
- 集合dirc记录剩余的合法字符串,每次扩展前,先将这些合法字符串从dirc中删除;for(string s : bfsFromBegin) dirc.erase(s);
- 始终对“自顶向下”bfsFromBegin队列进行判空操作(BFS算法中while(stack.empty()操作),只是利用reverseFlag将bfsFromBegin与bfsFromEnd进行调换,使得bfsFromBegin中始终保持扩展节点数较少;
- 根据reverseFlag进行构建树的过程:reverseFlag ? tree[s2].push_back(s1) : tree[s1].push_back(s2);// 构建树 并始终保持方向从beginWord指向endWord ;
while(!bfsFromBegin.empty()){ unordered_set<string> next; // 存放下一层需要遍历的节点(由于此处BFS的特殊性【一次性扩展所有节点】,所以不是直接添加到原先队列的末尾,而是借助next) for(string s : bfsFromBegin) // 遍历过的节点从set中删除 避免成环 dirc.erase(s); for(string s1 : bfsFromBegin){ // 扩展下一层的节点 for(int i = 0; i < s1.size(); ++i){ string s2 = s1; // s2记录由s1扩展而来字符串 !!!注意这条语句不要放错位置 for(char c = 'a'; c <= 'z'; ++c){ s2[i] = c; if(dirc.count(s2) == 0) continue; if(bfsFromEnd.count(s2)){ findFlag = true; // 找到双向BFS重合的字符串,BFS过程即可终止 }else{ next.insert(s2); // 将字符串加入扩展队列 } reverseFlag ? tree[s2].push_back(s1) : tree[s1].push_back(s2);// 构建树 并始终保持方向从beginWord指向endWord } } } bfsFromBegin = next; // 更新队列 if(bfsFromBegin.size() > bfsFromEnd.size()){ reverseFlag = !reverseFlag; // 取反 swap(bfsFromBegin, bfsFromEnd); // 交换BFS的队列 改变BFS的方向 } if(findFlag) break; // 双向BFS交汇 BFS过程终止 }
如何寻找最短路径?
至此,联通的树已经构建完毕,只需要从起点到终点进行遍历,符合条件的路径插入vector<vector<string> > ans中即可,由于是简单的遍历,采用DFS即可,代码如下:
void dfs(vector<string>& cur, string curWord, string endWord){ if(curWord == endWord){ ans.push_back(cur); return; } for(string s : tree[curWord]){ cur.push_back(s); dfs(cur, s, endWord); cur.pop_back(); } }
3,AC代码
class Solution {
public:
unordered_map<string, vector<string> > tree; // 构建图
vector<vector<string> > ans; // 存放最终结果
void dfs(vector<string>& cur, string curWord, string endWord){
if(curWord == endWord){
ans.push_back(cur);
return;
}
for(string s : tree[curWord]){
cur.push_back(s);
dfs(cur, s, endWord);
cur.pop_back();
}
}
vector<vector<string> > findLadders(string beginWord, string endWord, vector<string> & wordList) {
if(wordList.size() == 0 || find(wordList.begin(), wordList.end(), endWord) == wordList.end()) return {};
unordered_set<string> bfsFromBegin{beginWord}; // 自顶向下的BFS队列 !!!注意使用集合
unordered_set<string> bfsFromEnd{endWord}; // 自底向上的BFS队列 !!!注意使用集合
unordered_set<string> dirc(wordList.begin(), wordList.end()); // 初始化字典 记录未被访问过的字符串 !!!注意初始化方式
bool findFlag = false, reverseFlag = false; // findFlag两队列是否存在相同的元素 reverseflag时刻标记当前BFS遍历的方向(false为自顶向下,true为自底向上)
while(!bfsFromBegin.empty()){
unordered_set<string> next; // 存放下一层需要遍历的节点(由于此处BFS的特殊性【一次性扩展所有节点】,所以不是直接添加到原先队列的末尾,而是借助next)
for(string s : bfsFromBegin) // 遍历过的节点从set中删除 避免成环
dirc.erase(s);
for(string s1 : bfsFromBegin){ // 扩展下一层的节点
for(int i = 0; i < s1.size(); ++i){
string s2 = s1; // s2记录由s1扩展而来字符串 !!!注意这条语句不要放错位置
for(char c = 'a'; c <= 'z'; ++c){
s2[i] = c;
if(dirc.count(s2) == 0) continue;
if(bfsFromEnd.count(s2)){
findFlag = true; // 找到双向BFS重合的字符串,BFS过程即可终止
}else{
next.insert(s2); // 将字符串加入扩展队列
}
reverseFlag ? tree[s2].push_back(s1) : tree[s1].push_back(s2);// 构建树 并始终保持方向从beginWord指向endWord
}
}
}
bfsFromBegin = next; // 更新队列
if(bfsFromBegin.size() > bfsFromEnd.size()){
reverseFlag = !reverseFlag; // 取反
swap(bfsFromBegin, bfsFromEnd); // 交换BFS的队列 改变BFS的方向
}
if(findFlag) break; // 双向BFS交汇 BFS过程终止
}
vector<string> cur = {beginWord};
dfs(cur, beginWord, endWord); // 遍历形成的树 得到起点到终点的路径
return ans;
}
};
4,解题过程
想到了构建图,并且用BFS搜索最短路径,但是构建图没有好的想法,大概需要O(N^2),一次BFS即可找到所有最短的路径;
查阅了LeetCode提交记录中的题解,比较普遍的是利用双向BFS,并在BFS过程中构建树,最后利用DFS遍历构建的树并且记录通路;