BFS广度搜索及应用-单词接龙 II

一、关于树的BFS

1、无需分层遍历

    from collections import deque
    def levelOrderTree(root):
        if not root:
            return
              
        q = deque([root])
        while q:
            head = q.popleft()
            do something with this head node...
            if head.left:               
                q.append(head.left)
            if head.right:
                q.append(head.right)               
        return xxx

2、需要分层遍历

    def levelOrderTree(root):
        if not root:
            return
                  
        q = [root]
        while q:
            new_q = []
            for node in q: # 和上面代码相比 差异就在这里 和 deque
                do something with this layer nodes...
                if node.left:
                    new_q.append(node.left)
                if node.right:
                    new_q.append(node.right)           
            q = new_q
        return xxx

二、关于图的BFS

neighbor:邻接点。

set/seen:判断是否访问

1、无需分层遍历

  from collections import deque
  def bfs_graph(root): 
    if not root:
        return
     
    queue = deque([root])
    seen = set([root]) 
    while queue:
        head = queue.popleft()
        do something with this head...
        for neighbor in head.neighbors:
            if neighbor not in seen: # 和tree的区别无非就是多了一个是否访问过的判断
                seen.add(neighbor)
                queue.append(neighbor)
  return xxx

2、需分层遍历

    def bfs_graph(root):
        if not root:
            return []
                  
        q = [root]
        seen = set([root])
        while q:
            new_q = []
            for node in q:
                do something with this layer nodes...              
                for neighbor in node.neighbors:
                    if neighbor not in seen: # 和tree的区别无非就是多了一个是否访问过的判断
                        seen.add(neighbor)
                        new_q.append(neighbor)          
            q = new_q
        return xxx

三、拓扑排序

算法大致流程如下:

  1. 统计所有点的入度,并初始化拓扑序列为空。
  2. 将所有入度为 0 的点,也就是那些没有任何依赖的点,放到宽度优先搜索的队列中
  3. 将队列中的点一个一个的释放出来,放到拓扑序列中,每次释放出某个点 A 的时候,就访问 A 的相邻点(所有A指向的点),并把这些点的入度减去 1
  4. 如果发现某个点的入度被减去 1 之后变成了 0,则放入队列中。
  5. 直到队列为空时,算法结束
class Solution:
    """
    @param graph: A list of Directed graph node
    @return: Any topological order for the given graph.
    """
    def topSort(self, graph):
        node_to_indegree = self.get_indegree(graph)
  
        # bfs
        order = []
        start_nodes = [n for n in graph if node_to_indegree[n] == 0]
        queue = collections.deque(start_nodes)
        while queue:
            node = queue.popleft()
            order.append(node)
            for neighbor in node.neighbors:
                node_to_indegree[neighbor] -= 1
                if node_to_indegree[neighbor] == 0:
                    queue.append(neighbor)
                  
        return order
      
    def get_indegree(self, graph):
        node_to_indegree = {x: 0 for x in graph}
  
        for node in graph:
            for neighbor in node.neighbors:
                node_to_indegree[neighbor] += 1
                  
        return node_to_indegree

参考:https://www.cnblogs.com/bonelee/p/11724346.html

上个比较难的题-单词接龙 II

给定两个单词(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" 不在字典中,所以不存在符合要求的转换序列。
链接:https://leetcode-cn.com/problems/word-ladder-ii

先定义判断可以转换的方法:

def judge_same(str1, str2):
    n = 0
    for ch1, ch2 in zip(str1, str2):
        if ch1 != ch2:
            n += 1
        if n > 1:
            return False
    return n == 1

开始尝试字典+判断:

def findLadders(beginWord, endWord, wordList):
    if beginWord not in wordList:
        wordList.insert(0, beginWord)
    if endWord not in wordList:
        return []

    maps = {}
    for i in range(len(wordList)):
        for j in range(i + 1, len(wordList)):
            if judge_same(wordList[i], wordList[j]):
                if maps.get(wordList[i]):
                    maps.get(wordList[i]).append(wordList[j])
                else:
                    maps[wordList[i]] = [wordList[j]]
                if maps.get(wordList[j]):
                    maps.get(wordList[j]).append(wordList[i])
                else:
                    maps[wordList[j]] = [wordList[i]]
    print(maps)
    return maps

尝试好几次不过。。。过了29/39,,,大约5秒

接着尝试。。。不用字典的,用BFS,每一次添加节点都搜一遍

def findLadders(beginWord, endWord, wordList):
    if endWord not in wordList:
        return []

    dict = set(wordList)
    queue = [(beginWord, [beginWord])]
    res = {}
    depth = 0
    distance = {}
    min_len = float('inf')
    while queue:
        depth += 1
        new_queue = []
        for cur, path in queue:
            if cur == endWord:
                if len(path) < min_len:
                    min_len = len(path)
                    res[min_len] = [path]
                else:
                    res.get(min_len).append(path)
                    continue

            # 此处方法有多种
            for next in get_next_words(cur, dict):
                if distance.get(next) and distance.get(next) < depth:
                    continue
                distance[next] = depth
                if len(path) + 1 > min_len:
                    continue
                if next not in path:
                    new_queue.append((next, path + [next]))

        queue = new_queue

    return res.get(min_len)

方法一:遍历所有wordList

def get_next_words(word, wordList):
    words = []
    for next_word in wordList:
        if next_word != word and judge_same(word, next_word):
            words.append(next_word)
    return words

用时30多秒。。。还是不过

方法二:固定26个字母替换:

    def get_next_words(word, wordList):
        words = []
        for i in range(len(word)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                next_word = word[:i] + c + word[i + 1:]
                if next_word != word and next_word in wordList:
                    words.append(next_word)
        return words

更慢了,,,1分钟跑不完,,,要爆炸了。。。

方法三:用set替换list

再审审题,,,我去,,,这个题变态地方在字典,说是字典给的却是list

于是,有了画龙点睛之笔:word_set= set(wordList)

    def get_next_words(word, word_set):
        words = []
        for i in range(len(word)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                next_word = word[:i] + c + word[i + 1:]
                if next_word != word and next_word in word_set:
                    words.append(next_word)
        return words

终于过了,,,其实BFS还有好多的记忆搜索的优化,,不再赘述,,,

全代码如下:

class Solution:
    def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: List[List[str]]
        """
        def get_next_words(word, word_set, ch_set):
            words = []
            for i in range(len(word)):
                for c in ch_set:
                    next_word = ''.join((word[:i], c, word[i + 1:]))
                    if next_word != word and next_word in word_set:
                        words.append(next_word)
            return words
        
        if endWord not in wordList:
            return []

        word_set = set(wordList)    # 优化,减少遍历时间
        ch_set = set(''.join(wordList))        
        queue = [(beginWord, [beginWord])]
        res = []          # 存储结果
        depth = 0         # 深度,用于去除重复判断
        distance = {}     # 用于存储节点在哪一层遍历了
        while queue:
            depth += 1
            new_queue = []
            for cur, path in queue:
                if cur == endWord:
                    res.append(path)
                    continue   
                for next_word in get_next_words(cur, word_set, ch_set):
                    if distance.get(next_word, float('inf')) < depth:
                        continue
                    distance[next_word] = depth
                    new_queue.append((next_word, path + [next_word]))
            if res: # 这一层找到了一定是最短的
                break      
            queue = new_queue
        
        return res

最近又刷了一遍,发现不过了,32个用例超时,因为增加了特殊数据集~_~

双向 BFS介绍

什么是双向 BFS?

双向 BFS(Bidirectional Breadth-First Search)是一种图搜索算法,它从起点和终点同时开始搜索,以求得最短的路径。相比于普通的 BFS 算法,双向 BFS 算法可以减少搜索的节点数,从而提高搜索效率。

在双向 BFS 算法中,我们使用两个队列或集合分别记录从起点和终点出发的可达节点,并在每一轮中交替地从两个队列或集合中选择一个节点进行扩展。如果某个节点在两个方向上都被访问到了,那么说明在这两个节点之间存在一条路径。

双向 BFS 的优势

与普通的 BFS 算法相比,双向 BFS 算法具有以下优势:

搜索的节点数更少。由于双向 BFS 算法从两个方向同时开始搜索,因此它的搜索深度只需要普通 BFS 算法的一半,从而减少了搜索的节点数。

搜索的速度更快。由于双向 BFS 算法从起点和终点同时开始搜索,因此它的搜索速度更快。

能够避免无效搜索。由于双向 BFS 算法是从起点和终点同时开始搜索,因此它可以避免搜索那些无效的节点。

双向 BFS 的应用场景

双向 BFS 算法常用于求解最短路径问题,例如:

  • 单词接龙问题
  • 迷宫问题
  • 网络延迟时间问题
  • 火车换乘问题

双向 BFS 的代码实现

Python 代码实现如下:

def two_end_bfs(start, end):
    queue1, queue2 = [start], [end]
    visited1, visited2 = {start: 0}, {end: 0}
    while queue1 and queue2:
        cur_node1 = queue1.pop(0)
        for next_node in get_next_nodes(cur_node1):
            if next_node in visited2:
                return visited1[cur_node1] + visited2[next_node] + 1
            if next_node not in visited1:
                visited1[next_node] = visited1[cur_node1] + 1
                queue1.append(next_node)
        cur_node2 = queue2.pop(0)
        for next_node in get_next_nodes(cur_node2):
            if next_node in visited1:
                return visited1[next_node] + visited2[cur_node2] + 1
            if next_node not in visited2:
                visited2[next_node] = visited2[cur_node2] + 1
                queue2.append(next_node)
    return -1

写了如下代码:

def findLadders(beginWord, endWord, wordList):
    """
    :type beginWord: str
    :type endWord: str
    :type wordList: List[str]
    :rtype: List[List[str]]
    """

    def get_next_words(word, dict):
        words = set()
        for i in range(len(word)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                next_word = word[:i] + c + word[i + 1:]
                if next_word != word and next_word in dict:
                    words.add(next_word)
        return words

    if endWord not in wordList:
        return []

    word_set = set(wordList)  # 优
    word_set.add(beginWord)
    queue1 = {beginWord}
    queue2 = {endWord}
    visited1 = defaultdict(list)  # 用于存储节点在哪一层遍历了
    visited1[beginWord].append([beginWord])
    visited2 = defaultdict(list)  # 用于存储节点在哪一层遍历了
    visited2[endWord].append([endWord])
    reversed = False

    res = []  # 存储结果
    max_len = float('inf')
    depth = 0
    while queue1 and queue2:
        depth += 1
        if len(queue1) > len(queue2):
            queue1, queue2 = queue2, queue1
            visited1, visited2 = visited2, visited1
            beginWord, endWord = endWord, beginWord
            reversed = not reversed

        new_queue = set()
        visited = defaultdict(list)
        for cur in queue1:
            if cur == endWord:
                for path in visited1.get(cur):
                    if len(path) <= max_len and path not in res:
                        res.append(path)
                        max_len = len(path)
                continue

            for next_word in get_next_words(cur, word_set):
                if visited2.get(next_word):
                    for other_path in visited2.get(next_word):
                        for path in visited1.get(cur):
                            if reversed:
                                new_path = other_path + path
                            else:
                                new_path = path + other_path
                            if len(new_path) <= max_len and new_path not in res:
                                res.append(new_path)
                                max_len = len(new_path)

                if visited1.get(next_word):
                    continue
                for path in visited1.get(cur):
                    if reversed:
                        new_path = [next_word] + path
                    else:
                        new_path = path + [next_word]
                    if new_path not in visited[next_word]:
                        visited[next_word].append(new_path)
                new_queue.add(next_word)
        if res:
            return res
        queue1 = new_queue
        word_set -= new_queue
        visited1.update(visited)
    return res

32用例内存超限,因为存储路径太多了

接着试了BFS+DFS回溯法+邻接表,终于通过!!!

思路:

  1. 需要记录所有遍历过的单词还有前后关系, 如 adjoin_list= { next_word :[pre1, pre2,...,preN]},其中preN为前序节点。
  2. 再用一个word_level保存每一层的单词 word_level= {1:[word1 ,word2 ...]}
  3. BFS遍历的时候,判断每一层单词的关联单词是否在wordList ,并且是否已经被记录过了,如果没有记录过 就把word加入adjoin_list和 word_level ,并且把它的后序单词加入new_queue,直到遇到endWord 这样当我们进行BFS遍历的时候,直到遇到endWord(最后这层需要遍历完,因为可能存在多个单词都能到end的情况)。
  4. 得到了一个保存每一层有哪些word,和其中哪些word存在上下层的连接关系的list,我们再从endWord,用一个dfs来还原bfs的遍历顺序,得到result。

代码如下:

def findLadders(beginWord, endWord, wordList):
    """
    :type beginWord: str
    :type endWord: str
    :type wordList: List[str]
    :rtype: List[List[str]]
    """

    def get_next_words(word, word_set):
        words = set()
        for i in range(len(word)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                next_word = word[:i] + c + word[i + 1:]
                if next_word != word and next_word in word_set:
                    words.add(next_word)
        return words

    if endWord not in wordList:
        return []

    word_set = set(wordList)  # 查询提高速度
    adjoin_list = defaultdict(set)  # 邻接矩阵,存前向节点,不是后向节点的原因是判重很方便
    word_level = defaultdict(set)   # 记录每一层遍历遇到的单词
    word_level[0].add(beginWord)
    depth = 0
    find_flag = False # 是否找到

    queue = {beginWord}
    while queue:
        new_queue = set()
        depth += 1
        for cur in queue:
            for next_word in get_next_words(cur, word_set):
                if next_word == endWord: # 如果遇到终点,则标记True,这一层走完就跳出循环
                    find_flag = True
                if next_word not in adjoin_list:
                    new_queue.add(next_word)
                    word_level[depth].add(next_word)
                adjoin_list[next_word].add(cur)

        if find_flag:  # 这一层找到了一定是最短的
            break
        queue = new_queue

    if not find_flag:  # 没找到
        return []

    # dfs回溯路径
    def dfs(depth, tmp, next_word):
        if depth == -1:
            return [tmp]
        res = []
        for cur_word in word_level[depth]:
            if cur_word in adjoin_list[next_word]:
                res.extend(dfs(depth - 1, [cur_word] + tmp, cur_word))
        return res

    return dfs(len(word_level) - 2, [endWord], endWord)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值