每日刷题 2022.07.13
- leetcode原题链接: leetcode.cn/problems/wo…
- 难度:困难
- 方法:双向bfs, 朴素bfs
题目
- 字典 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
- beginWord、endWord 和 wordList[i] 由小写英文字母组成
- beginWord != endWord
- wordList 中的所有字符串 互不相同
解题思路
- 看了三叶姐的题解后,书写的双向
bfs,初次学习。
为什么要用双向bfs?
- 因为对于本题来说,
beginword的长度最长是10,那么可以由beginword产生10 * 25个新单词,也就是第一层会产生250个新单词,那么第二层就会产生250 * 10 * 25 = 62500 ~~ 6 * 10 ^ 4个新单词,第三层会产生62500 * 10 * 25 = 15625000 ~~ 15 * 10 ^ 6, 再往下的话,就会产生越来越多的新单词。 - 随着层数的增加,产生的新的单词数增加的速度很快,有可能会出现「搜索空间爆炸」的问题。那么此时朴素的
bfs会出现问题,那么就可以使用 双向bfs来解决。 - 区别:
- 朴素
bfs,空间取决于搜索空间中的最大宽度。一般都是从一个节点一直向下,逐层遍历,直到找到终点🏁。 - 双向
bfs,可以同时从两个方向开始搜索🔍,如果两个方向的搜索找到了相同的点,那么就意味着找到了联通起点和终点的最短路径。
- 朴素
- 适用情况:有解、有一定数据范围同时层级节点数量以倍数或者指数级别增长的情况,双向
bfs的搜索空间通常只有朴素bfs的空间消耗的几百分之一,甚至几千分之一。
双向bfs如何实现呢?
- 创建两个队列,用来存储两个方向的开始节点,从两个方向开始遍历。
- 创建两个哈希表{key: val} == {节点: 转换次数},用于解决相同节点重复搜索和记录转换次数。
- 为了尽可能让两个搜索方向平均(这样可以防止一边倒的情况使空间爆炸),每次需要选择队列长度较短的进行拓展。
- 如果在搜索过程中,当前的节点存在于两个哈希表中,就表示找到了最短路径,即:两个集合中存储的当前节点的转换次数相加。
注意⚠️
- (
js中优化队列的操作)如果是传递参数的方式书写的bfs,就不能直接在函数内部书写qq = q,因为仅仅只是在函数内修改了qq,函数结束,就会将其销毁,并不会影响外部的qq队列。
AC代码
- 双向
bfs
/**
* @param {string} beginWord
* @param {string} endWord
* @param {string[]} wordList
* @return {number}
*/
var ladderLength = function(beginWord, endWord, wordList) {
// 双向bfs
// 首先先将所有列表中的数存放到集合中
let set = new Set(wordList);
// 不存在末尾的单词的时候,就直接返回0
if(!set.has(endWord)) return 0;
// 实现bfs
let ans = bfs();
return ans == -1 ? 0 : ans;
// 双向bfs
function bfs() {
// 声明两个队列、两个map
let que1 = [beginWord],que2 = [endWord];
// Map集合记录当前这个单词,是在第几层被遍历到的,这样就不需要vis数组了
let map1 = new Map(), map2 = new Map();
map1.set(beginWord, 0);
map2.set(endWord, 0);
// 需要平衡一下两个队列的大小,让小的先找
while (que1.length != 0 && que2.length != 0) {
let nn = -1;
// 不等于0才需要执行
if(que1.length <= que2.length) {
nn = update(que1, map1, map2);
}else {
nn = update(que2, map2, map1);
}
if(nn != -1) return nn;
}
return -1;
}
function update (q, m1, m2) {
// 层序遍历一层,完整的
let len = q.length;
for(let i = 0; i < len; i++) {
let cur = q.shift(), curLen = cur.length;
// 循环判断修改其每一位
for(let z = 0; z < curLen; z++) {
for(let j = 0; j < 26; j++) {
// 替换每一位
let str = cur.substr(0, z) + String.fromCharCode('a'.charCodeAt() + j) + cur.substr(z + 1);
// 需要判断当前拼接后的是否符合要求
if(set.has(str)) {
// 然后再进一步判断
// 判断是否在当前方向遍历过
if(m1.has(str)) continue;
// 判断另一个集合中是否遍历到,如果遍历到,就直接返回
if(m2.has(str)) {
// 找到相遇的节点,返回结果
// 因为步数是从0开始的,因此这里需要加上两个初始节点的步数
return m1.get(cur) + m2.get(str) + 2;
} else {
// 不是相遇的节点,还需要遍历
// q.push(str);
m1.set(str, m1.get(cur) + 1);
}
}
}
}
}
// q = qq;
return -1;
}
};