力扣高频|算法面试题汇总(一):开始之前
力扣高频|算法面试题汇总(二):字符串
力扣高频|算法面试题汇总(三):数组
力扣高频|算法面试题汇总(四):堆、栈与队列
力扣高频|算法面试题汇总(五):链表
力扣高频|算法面试题汇总(六):哈希与映射
力扣高频|算法面试题汇总(七):树
力扣高频|算法面试题汇总(八):排序与检索
力扣高频|算法面试题汇总(九):动态规划
力扣高频|算法面试题汇总(十):图论
力扣高频|算法面试题汇总(十一):数学&位运算
力扣高频|算法面试题汇总(十):图论
力扣链接
目录:
- 1.单词接龙
- 2.岛屿数量
- 3.课程表
- 4.课程表 II
1.单词接龙
给定两个单词(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。
思路:
参考官方思路:广度优先搜索
用一个图来模拟整个流程,拥有一个 beginWord
和一个 endWord
,分别表示图上的 start node
和 end node
。中间节点是 wordList
给定的单词。对这个单词接龙每个步骤的唯一条件是相邻单词只可以改变一个字母。
将问题抽象在一个无向无权图中,每个单词作为节点,差距只有一个字母的两个单词之间连一条边。问题变成找到从起点到终点的最短路径,如果存在的话。因此可以使用广度优先搜索方法。算法中最重要的步骤是找出相邻的节点,也就是只差一个字母的两个单词。为了快速的找到这些相邻节点,对给定的 wordList 做一个预处理,将单词中的某个字母用 * 代替
这个预处理构造了一个单词变换的通用状态。例如:Dog ----> D*g <---- Dig
,Dog
和 Dig
都指向了一个通用状态 D*g
。这步预处理找出了单词表中所有单词改变某个字母后的通用状态,并更方便也更快的找到相邻节点。否则,对于每个单词需要遍历整个字母表查看是否存在一个单词与它相差一个字母,这将花费很多时间。预处理操作在广度优先搜索之前高效的建立了邻接表。
在广搜时需要访问 Dug 的所有邻接点,可以先生成 Dug 的所有通用状态:
- 1.
Dug => *ug
- 2.
Dug => D*g
- 3.
Dug => Du*
第二个变换 D*g
可以同时映射到 Dog
或者 Dig
,因为他们都有相同的通用状态。拥有相同的通用状态意味着两个单词只相差一个字母,他们的节点是相连的。
算法步骤:
- 1.先对给定的
wordList
进行预处理,将通用状态记录下来,键是通用状态,值是所有具有通用状态的单词。 - 2.将包含
beginWord
和1
成对放入队列中,需要返回endWord
的层次也就是从beginWord
出发的最短距离。 - 3.使用
visited
记录访问的节点,避免重复访问,出现环。 - 4.当队列中有元素的时候,取出第一个元素,记为
current_word
。 - 5.找到
current_word
的所有通用状态,并检查这些通用状态是否存在其它单词的映射,这一步通过检查all_combo_dict
来实现。 - 6.从
all_combo_dict
获得的所有单词,都和current_word
共有一个通用状态,所以都和current_word
相连,因此将他们加入到队列中。 - 7.对于新获得的所有单词,向队列中加入元素
(word, level + 1)
其中level
是current_word
的层次。 - 8.最终到达期望的单词,对应的层次就是最短变换序列的长度。标准广度优先搜索的终止条件就是找到结束单词。
复杂度分析:
时间复杂度: O ( M × N ) O(M \times N) O(M×N),其中 M M M 是单词的长度 N N N 是单词表中单词的总数。找到所有的变换需要对每个单词做 M M M 次操作。同时,最坏情况下广度优先搜索也要访问所有的 N N N 个单词。
空间复杂度: O ( M × N ) O(M \times N) O(M×N),要在 all_combo_dict
字典中记录每个单词的 M M M 个通用状态。访问数组的大小是 N N N。广搜队列最坏情况下需要存储 N N N 个单词。
C++
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
if(find(wordList.begin(), wordList.end(), endWord) == wordList.end()) return 0;
// 可重复map 记录通用状态
unordered_multimap<string, string> all_combo_dict;
unordered_set<string> visited;
for(auto word : wordList){
string str = word;
for(int i = 0; i < word.size(); ++i){
str[i] = '*';
all_combo_dict.emplace(str, word);
str[i] = word[i];
}
}
// 构造队列
queue<string> wordQueue;
wordQueue.push(beginWord); // 添加第一个元素
int level = 1;
while(!wordQueue.empty()){
++level;
int length = wordQueue.size();
while(length--){
string cur = wordQueue.front(); // 获取队列中的第一个元素
wordQueue.pop();
for(int i = 0; i < cur.size(); ++i){
char tmp = cur[i];
cur[i] = '*'; // 修改成通用形式
// equal_range 返回范围[first,last)内等于指定值val的子范围的迭代器。
// 注意的是使用这个函数的前提是范围[first,last)内的元素是有序的。
// 同时注意函数的返回值类型,返回值是个pair对象,pair的first是左边界的迭代器,
// pair的second是右边界的迭代器。
// 区间是左闭右开的,[左边界,右边界)。
auto range = all_combo_dict.equal_range(cur);
for(auto itear = range.first; itear != range.second; ++itear){
if(visited.count(itear->second) == 0){
// 如果还没有访问
if(itear->second == endWord) return level; // 如果找到,返回结果
wordQueue.push(itear->second);
visited.emplace(itear->second);
}
}
cur[i] = tmp; // 还原
}
}
}
return 0;
}
};
Python
# defaultdict构造有默认输出的字典
from collections import defaultdict
class Solution(object):
def ladderLength(self, beginWord, endWord, wordList):
"""
:type beginWord: str
:type endWord: str
:type wordList: List[str]
:rtype: int
"""
if not endWord in wordList or not beginWord or not endWord or not wordList:
return 0
# 获取单词的长度
length = len(beginWord)
# 字典用来存放任何给定单词的组合词。一次换一个字母
all_combo_dict = defaultdict(list)
for word in wordList:
for i in range(length):
# 键是通用词
# 值是具有相同中间泛型单词的单词列表
all_combo_dict[word[:i] + "*" + word[i+1:]].append(word)
# 队列BFS
queue = [(beginWord, 1)]
# Visited以确保不会重复处理相同的字
visited = {
beginWord: True}
while queue:
current_word, level = queue.pop(0)
for i in range(length):
# 现在词的中间词
intermediate_word = current_word[:i] + "*" + current_word[i+1:]
# 下一个状态是所有中间状态相同的词。
for word in all_combo_dict[intermediate_word]:
# 如果在任何时候,如果找到要找的东西,即结束词,可以返回答案。
if word == endWord:
return level + 1
# 否则,将其添加到BFS队列。也标志着它访问
if word not in visited:
visited[word] = True
queue.append((word, level + 1))
all_combo_dict[intermediate_word] = [] # 有visited 这个可以不加
return 0
思路2:
参考官方思路:双向广度优先搜索
在思路1中,根据给定字典构造的图可能会很大,而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。假如每个节点的分支数量相同,搜索空间会随着层数的增长指数级的增加。
如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord
开始,另一边从 endWord
开始。每次从两边各扩展一个节点,当发现某一时刻两边都访问了某一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而降低时间和空间复杂度。
算法步骤:
- 1.算法核心和思路1相似,不过从两个节点同时开始搜索,同时搜素的结束条件也有所变化。
- 2.使用两个访问数组,分别记录从对应的起点是否已经访问了该节点。
- 3.如果发现一个节点被两个搜索同时访问,就结束搜索过程。因为找到了双向搜索的交点。过程如同从中间相遇而不是沿着搜索路径一直走。双向搜索的结束条件是找到一个单词被两边搜索都访问过了。
- 4.最短变换序列的长度就是中间节点在两边的层次之和。因此可以在访问数组中记录节点的层次。
复杂度分析:
时间复杂度: O ( M × N ) O(M \times N) O(M×N),其中 M M M 是单词的长度 N N N 是单词表中单词的总数。找到所有的变换需要对每个单词做 M M M 次操作。但是搜索时间会被缩小一半,因为两个搜索会在中间某处相遇。
空间复杂度: O ( M × N ) O(M \times N) O(M×N),要在 all_combo_dict
字典中记录每个单词的 M M M 个通用状态。访问数组的大小是 N N N。但是因为会在中间相遇,所以双向搜索的搜索空间变小。
C++
class Solution {
public:
unordered_map<string, vector<string>> all_combo_dict;
int length;
int visitWordNode(queue<pair<string, int>>& que,
unordered_map<string, int>& visited,
unordered_map<string, int>& others_visited){
string current_word = que.front().first;
int level = que.front().second;
que.pop();
for(int i = 0; i < length; ++i){
string index = current_word.substr(0, i)+