【双向BFS】LeetCode 127. 单词接龙
题目链接
思路
本题最朴素的解法是运用 BFS 求解,从 beginWord 出发,枚举所有替换一个字符的方案,如果方案存在于 wordList 中,则加入队列中,这样队列中就存在所有替换次数为 1 的单词。然后从队列中取出元素,继续这个过程,直到遇到 endWord 或者队列为空为止。
同时为了「防止重复枚举到某个中间结果」和「记录每个中间结果是经过多少次转换而来」,我们需要建立一个「哈希表」进行记录。哈希表的 KV 形式为 { 单词 : 由多少次转换得到 }。当枚举到新单词 str 时,需要先检查是否已经存在与「哈希表」中,如果不存在则更新「哈希表」并将新单词放入队列中。
这样的做法可以确保「枚举到所有由 beginWord 到 endWord 的转换路径」,并且由 beginWord 到 endWord 的「最短转换路径」必然会最先被枚举到。
但是这个解法的搜索空间十分巨大,所以可以采用双向BFS的方法求解,即从 beginWord 和 endWord 同时进行 BFS 搜索。
「双向 BFS」的基本实现思路如下:
- 创建「两个队列」分别用于两个方向的搜索;
- 创建「两个哈希表」用于「解决相同节点重复搜索」和「记录转换次数」;
- 为了尽可能让两个搜索方向“平均”,每次从队列中取值进行扩展时,先判断哪个队列容量较少;
- 如果在搜索过程中「搜索到对方搜索过的节点」,说明找到了最短路径。
代码
class Solution {
String beginWord, endWord;
Set<String> set = new HashSet<>();
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
this.beginWord = beginWord;
this.endWord = endWord;
// 将所有 word 存入 set,如果目标单词不在 set 中,说明无解
set.addAll(wordList);
if(!set.contains(this.endWord)){
return 0;
}
return bfs() + 1;
}
int bfs() {
// d1 代表从起点 beginWord 开始搜索(正向)
// d2 代表从结尾 endWord 开始搜索(反向)
Deque<String> d1 = new ArrayDeque<>(), d2 = new ArrayDeque();
// m1 和 m2 分别记录两个方向出现的单词是经过多少次转换而来
// e.g.
// m1 = {"abc":1} 代表 abc 由 beginWord 替换 1 次字符而来
// m2 = {"xyz":3} 代表 xyz 由 endWord 替换 3 次字符而来
Map<String, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
d1.add(beginWord);
m1.put(beginWord, 0);
d2.add(endWord);
m2.put(endWord, 0);
// 只有两个队列都不空,才有必要继续往下搜索
// 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
// e.g.
// 如果 d1 为空了,说明从 beginWord 搜索到底都搜索不到 endWord,反向搜索也没必要进行了
while(!d1.isEmpty() && !d2.isEmpty()){
int t = -1;
// 为了让两个方向的搜索尽可能平均,优先拓展队列内元素少的方向
if(d1.size() <= d2.size()){
t = update(d1, m1, m2);
}else{
t = update(d2, m2, m1);
}
if(t != -1){
return t;
}
}
return -1;
}
// update 代表从 deque 中取出一个单词进行扩展,
// current 为当前方向的距离字典;other 为另外一个方向的距离字典
int update(Deque<String> deque, Map<String, Integer> current, Map<String, Integer> other) {
int m = deque.size();
while(m-- > 0){
// 获取当前需要扩展的原字符串
String string = deque.pollFirst();
int n = string.length();
// 枚举替换原字符串的哪个字符 i
for(int i = 0; i < n; i++){
// 枚举将 i 替换成哪个小写字母
for(int j = 0; j < 26; j++){
// 替换后的字符串
String nextString = string.substring(0, i) + String.valueOf((char) ('a' + j)) +
string.substring(i + 1);
if(set.contains(nextString)){
// 如果该字符串在「当前方向」被记录过(拓展过),跳过即可
if(current.containsKey(nextString) && current.get(nextString) <= current.get(string) + 1){
continue;
}
// 如果该字符串在「另一方向」出现过,说明找到了联通两个方向的最短路
if(other.containsKey(nextString)){
return current.get(string) + 1 + other.get(nextString);
}else{
// 否则加入 deque 队列
deque.addLast(nextString);
current.put(nextString, current.get(string) + 1);
}
}
}
}
}
return -1;
}
}