题目
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord
的最短转换序列的长度。转换需遵循如下规则:每次转换只能改变一个字母。 转换过程中的中间单词必须是字典中的单词。 说明:
如果不存在这样的转换序列,返回 0。 所有单词具有相同的长度。 所有单词只由小写字母组成。 字典中不存在重复的单词。 你可以假设
beginWord 和 endWord 是非空的,且二者不相同。示例 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” 不在字典中,所以无法进行转换。
给出函数头:
class Solution
{
public:
int ladderLength(string beginWord, string endWord, vector& wordList) {}
};来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/word-ladder
题解
思路一
每个单词都为无向图中的一个节点,相差一个字母的一对单词即视为相连的一对节点,抽象为求图中某两个节点之间的路径长度。在此使用广度优先搜索法(BFS)。
首先如何判断节点相连:
bool isLinked(string str1, string str2)
{
int count = 0;
for (int i = 0; i < str1.length(); ++i)
{
if (str1[i] != str2[i])
++count;
if (count > 1)//不等字符数大于一便不相连
return false;
}
return true;
}
有了节点表和判断是否相连的条件,便可进行BFS搜索
int ladderLength(string beginWord, string endWord, vector<string>& wordList)
{
unordered_map<string, bool> umap;//记录节点是否被访问过
queue<pair<string,int>> que;//string为单词的值,int为起始节点到当前节点所需最小步数
que.push(make_pair(beginWord, true));//起点节点入队
while (!que.empty())
{
string cur_node = que.front().first;
int step = que.front().second;
que.pop();
for (auto var : wordList)
{
if (isLinked(cur_node, var) && umap.find(var) == umap.end())
{
if (var == endWord)
return step + 1;
que.push(make_pair(var, step + 1));
umap[var] = true;
}
}
}
return 0;
}
结论:逻辑正确,但在LeetCode上超时无法通过,原因在于每个单词都要与其他所有单词比对是否“连接”,时间复杂度为O(n^2),并且没有记录比对结果,存在大量重复比对。
思路二
改进比对效率,引入字典。
拥有一个 beginWord 和一个 endWord,分别表示图上的 start node 和 end node。我们希望利用一些中间节点(单词)从 start node 到 end node,中间节点是 wordList 给定的单词。我们对这个单词接龙每个步骤的唯一条件是相邻单词只可以改变一个字母。
我们将问题抽象在一个无向无权图中,每个单词作为节点,差距只有一个字母的两个单词之间连一条边。问题变成找到从起点到终点的最短路径,如果存在的话。因此可以使用广度优先搜索方法。
算法中最重要的步骤是找出相邻的节点,也就是只差一个字母的两个单词。为了快速的找到这些相邻节点,我们对给定的 wordList 做一个预处理,将单词中的某个字母用 * 代替。
这个预处理帮我们构造了一个单词变换的通用状态。例如:Dog ----> Dg <---- Dig,Dog 和 Dig 都指向了一个通用状态 Dg。
这步预处理找出了单词表中所有单词改变某个字母后的通用状态,并帮助我们更方便也更快的找到相邻节点。否则,对于每个单词我们需要遍历整个字母表查看是否存在一个单词与它相差一个字母,这将花费很多时间。预处理操作在广度优先搜索之前高效的建立了邻接表。
例如,在广搜时我们需要访问 Dug 的所有邻接点,我们可以先生成 Dug 的所有通用状态:
Dug => ug
Dug => Dg
Dug => Du*
第二个变换 D*g 可以同时映射到 Dog 或者 Dig,因为他们都有相同的通用状态。拥有相同的通用状态意味着两个单词只相差一个字母,他们的节点是相连的。
来源:力扣(LeetCode)
建立字典:
//string为字典目录,例如'*bc'。vector<string>为相应目录下对应的单词,例如'abc','bbc'就在目录'*bc'下
unordered_map<string, vector<string>> dir_map;
for (auto var : wordList)
{
for (int i = 0; i < word_len; ++i)
{
string dir_str = var.substr(0, i) + "*" + var.substr(i + 1, word_len);
dir_map[dir_str].push_back(var);
}
}
完整函数:
int ladderLength1(string beginWord, string endWord, vector<string>& wordList) {
unordered_map<string, bool> flag_map;
unordered_map<string, vector<string>> dir_map;
queue<pair<string, int>> que;
int word_len = beginWord.length();
que.push(make_pair(beginWord, 1));
for (auto var : wordList)
{
for (int i = 0; i < word_len; ++i)
{
string dir_str = var.substr(0, i) + "*" + var.substr(i + 1, word_len);
dir_map[dir_str].push_back(var);
}
}
vector<string> vec;
while (!que.empty())
{
string cur_node = que.front().first;
int step = que.front().second;
que.pop();
for (int i = 0; i < word_len; ++i)
{
string dir_str = cur_node.substr(0, i) + "*" + cur_node.substr(i + 1, word_len);
for (auto var : dir_map[dir_str])
{
if (var == endWord)
{
return step + 1;
}
if (flag_map.find(var) == flag_map.end())
{
vec.push_back(var);
que.push(make_pair(var, step + 1));
flag_map[var] = true;
}
}
}
}
return 0;
}
结论:在LeetCode上可以通过,但时间效率仍可优化。
思路三
引入双向BFS,使算法时间更为稳定。
即从起点->终点,和终点->起点两个方向同时搜索,两次搜索在半路相遇时终止。
两个疑惑点:如何“同时”,怎么判断“相遇”?
关于“同时”:方案1.多开一个线程从尾到头搜索。方案2.在单线程环境下,根据贪心算法思想,选择当前分支较少的方向推进。
关于“相遇”:两个搜索方向各建立一个访问表,遇节点时不仅比对当前方向的访问表,同时比对相逆方向的访问表,一旦当前访问节点在相逆方向的访问表中被标记,即为相遇。
方案2代码:
int ladderLength(string beginWord, string endWord, vector<string>& wordList)
{
unordered_map<string, int> b_visited_map, e_visited_map;
unordered_map<string, vector<string>> dir_map;
queue<pair<string, int>> que_b,que_e;
int word_len = beginWord.length();
wordList.push_back(beginWord);//放入起始节点,否则从尾到头搜索不到
que_b.push(make_pair(beginWord, 1));
que_e.push(make_pair(endWord, 1));
bool isEndExist = false;
for (auto var : wordList)
{
for (int i = 0; i < word_len; ++i)
{
string dir_str = var.substr(0, i) + "*" + var.substr(i + 1, word_len);
dir_map[dir_str].push_back(var);
}
if (var == endWord)
isEndExist = true;
}
if (!isEndExist)
return 0;
while (!que_b.empty() && !que_e.empty())
{
queue<pair<string, int>> &que = que_b.size() > que_e.size() ? que_e : que_b;//选择当前分支较少的方向推进
int node_count = que.size();
for (int i = 0; i < node_count; ++i)//每次搜索需清空上次搜索入队的分支节点,否则最终路径可能不为最短
{
string cur_node = que.front().first;
int step = que.front().second;
que.pop();
for (int i = 0; i < word_len; ++i)
{
string dir_str = cur_node.substr(0, i) + "*" + cur_node.substr(i + 1, word_len);
for (auto var : dir_map[dir_str])
{
if (var == endWord && que == que_b)
return step + 1;//从头到尾搜索成功
if (var == beginWord && que == que_e)
return step + 1;//从尾到头搜索成功
if (que == que_b)
{
if (b_visited_map.find(var) == b_visited_map.end())
{
que.push(make_pair(var, step + 1));
b_visited_map[var] = step + 1;
if (e_visited_map.find(var) != e_visited_map.end())
{
return step + 1 + e_visited_map.find(var)->second - 1;//中途相遇
}
}
}
else
{
if (e_visited_map.find(var) == e_visited_map.end())
{
que.push(make_pair(var, step + 1));
e_visited_map[var] = step + 1;
if (b_visited_map.find(var) != b_visited_map.end())
{
return step + 1 + b_visited_map.find(var)->second - 1;//中途相遇
}
}
}
}
}
}
}
return 0;
}