Python - 深夜数据结构与算法之 Two-Ended BFS

目录

一.引言

[二.双向 BFS 简介](#二.双向 BFS 简介)

1.双向遍历示例

2.搜索模版回顾

三.经典算法实战

[1.Word-Ladder [127]](#1.Word-Ladder [127])

[2.Min-Gen-Mutation [433]](#2.Min-Gen-Mutation [433])

四.总结


一.引言

DFS、BFS 是常见的初级搜索方式,为了提高搜索效率,衍生了剪枝、双向 BFS 以及 A* 即启发式搜索等高级搜索方式。剪枝通过避免不必要或者次优解来减少搜索的次数,提高搜索效率;双向 BFS 通过层序遍历从首尾逼近答案,提高搜索效率;启发式搜索则是从优先级的角度出发,基于优先级高低搜索,提高搜索效率。本文主要介绍双向 BFS 的使用。

二.双向 BFS 简介

1.双向遍历示例

双向连通图

求 A -> L 所需最短路径。

遍历层级关系

不同颜色代表不同层级的 BFS,绿色为 root,蓝色为第二层,从左向右递推。

双向遍历

从 A/L 同时层序遍历,当二者扩散的点重合时,左右路径长度相加即为最短路径。

2.搜索模版回顾

◆ DFS - 递归

◆ DFS - 非递归

◆ BFS - 栈

三.经典算法实战

1.Word-Ladder [127]

单词接龙: https://leetcode.cn/problems/word-ladder/description/

◆ 单向 BFS

python 复制代码
class Solution:    
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        valid_word = set(wordList)

        if endWord not in valid_word:
            return 0

        stack = [(beginWord, 1)]

        while stack:
            word, level = stack.pop(0)

            for i in range(len(word)):
                for char in "abcdefghijklmnopqrstuvwxyz":
                    new_word = word[:i] + char + word[i + 1:]

                    if new_word == endWord:
                        return level + 1
                    elif new_word in valid_word:
                        stack.append((new_word, level + 1))
                        valid_word.remove(new_word)

        return 0

这里我们可以打印一下转换的流程图,hot 有多层 level 出发,第二条路径走到了 cog,即结束遍历,当然 log 也可以走到 cog 只不过已经不需要了。

hot 2 -> lot 3

hot 2 -> dot 3 -> dog 4 -> cog 5

hot 2 -> dot 3 -> log 4

◆ 双向 BFS

python 复制代码
class Solution(object):
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        # 去重使用
        valid_word = set(wordList)

        # 边界条件
        if endWord not in wordList or len(wordList) == 0:
            return 0

        # 双向 BFS
        begin, end, step = {beginWord}, {endWord}, 1


        # 同时有元素才能继续,如果一遍没元素代表已中断,无法联通,直接结束
        while begin and end:

            # 减少排查的可能性,从单词少的方向排查,避免无效查询
            if len(begin) > len(end):
                begin, end = end, begin

            # 存储下一层
            next_level = set()
            # 遍历下一层的多个结果
            for word in begin:
                # 遍历每个位置
                for i in range(len(word)):
                    # a-z
                    for char in "abcdefghijklmnopqrstuvwxyz":
                        # 节省无必要的替换
                        if char != word[i]:
                            new_word = word[:i] + char + word[i + 1:]
                            # 二者相遇即返回
                            if new_word in end:
                                return step + 1
                            if new_word in valid_word:
                                next_level.add(new_word)
                                valid_word.remove(new_word)

            # 指针替换
            begin = next_level
            step += 1

        return 0

已经将详细的注释加在代码里了,从 {start},{end} 两个方向查找,每次只找短的缩小无效查询的次数,这其实也是一种剪枝的策略,正所谓图中有真意欲辨已忘言:

◆ 双向 BFS + 剪枝

python 复制代码
class Solution(object):
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        # 去重使用
        valid_word = set(wordList)

        if endWord not in wordList or len(wordList) == 0:
            return 0

        # 剪枝优化
        s = set()
        for word in wordList:
            for char in word:
                s.add(char)

        s = ''.join(list(s))

        # 双向 BFS
        begin, end, step = {beginWord}, {endWord}, 1

        while begin and end:

            if len(begin) > len(end):
                begin, end = end, begin

            # 存储下一层
            next_level = set()
            for word in begin:
                for i in range(len(word)):
                    # a-z
                    for char in s:
                        # 节省无必要的替换
                        if char != word[i]:
                            new_word = word[:i] + char + word[i + 1:]

                            if new_word in end:
                                return step + 1
                            if new_word in valid_word:
                                next_level.add(new_word)
                                valid_word.remove(new_word)

            # 指针替换
            begin = next_level
            step += 1

        return 0

上面的两个方法在构建 new_word 时都遍历了所有 26 个字母 char,其实我们可以根据 end_word 的去重字符进行状态空间压缩,从而减少无意义的遍历,因为 char not in end_word 则 new_word 必定 not in end_word,从而优化时间复杂度。

2.Min-Gen-Mutation [433]

最小基因突变: https://leetcode.cn/problems/minimum-genetic-mutation/description/

◆ BFS

python 复制代码
class Solution(object):
    def minMutation(self, startGene, endGene, bank):
        """
        :type startGene: str
        :type endGene: str
        :type bank: List[str]
        :rtype: int
        """
        if not bank:
            return -1

        bank = set(bank)
        if endGene not in bank:
            return -1

        stack = [(startGene, 0)]

        while stack:
            gene, level = stack.pop(0)

            for i in range(len(gene)):
                for char in "ACGT":
                    new_gene = gene[:i] + char + gene[i + 1:]

                    if new_gene == endGene:
                        return level + 1

                    if new_gene in bank:
                        stack.append((new_gene, level + 1))
                        bank.remove(new_gene)

        return -1

和上一题异曲同工之妙,只不过从单词接龙变成基因 🧬 接龙,每次修改的地方有限。

◆ 双向 BFS

python 复制代码
class Solution(object):
    def minMutation(self, startGene, endGene, bank):
        """
        :type startGene: str
        :type endGene: str
        :type bank: List[str]
        :rtype: int
        """
        if not bank:
            return -1

        bank = set(bank)
        if endGene not in bank:
            return -1

        # 初始化首尾
        front, back, step = {startGene}, {endGene}, 0

        while front and back:

            next_front = set()

            # 遍历当前层 Gene
            for gene in front:
                print(gene)
                for i in range(len(gene)):
                    for char in "ACGT":
                        new_gene = gene[:i] + char + gene[i + 1:]
                        # 相遇了
                        if new_gene in back:
                            return step + 1
                        # 下一层突变
                        if new_gene in bank:
                            next_front.add(new_gene)
                            bank.remove(new_gene)

            # 取短的遍历加速
            if len(next_front) > len(back):
                front, back = back, next_front
            else:
                front = next_front

            step += 1

        return -1

和上面异曲同工,老曲新唱,相当于再温习一遍。其加速点就是左右替换,优先遍历可能性少的情况。

四.总结

这节内容 '双向 BFS' 起始也包含着很多剪枝的策略,所以其也属于优化搜索方式的方法之一,下一节我们介绍高级搜索的最后一块内容: A* 启发式搜索。

相关推荐
幸运超级加倍~9 分钟前
软件设计师-上午题-16 算法(4-5分)
笔记·算法
yannan2019031316 分钟前
【算法】(Python)动态规划
python·算法·动态规划
埃菲尔铁塔_CV算法18 分钟前
人工智能图像算法:开启视觉新时代的钥匙
人工智能·算法
EasyCVR19 分钟前
EHOME视频平台EasyCVR视频融合平台使用OBS进行RTMP推流,WebRTC播放出现抖动、卡顿如何解决?
人工智能·算法·ffmpeg·音视频·webrtc·监控视频接入
linsa_pursuer20 分钟前
快乐数算法
算法·leetcode·职场和发展
小芒果_0121 分钟前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
qq_4340859022 分钟前
Day 52 || 739. 每日温度 、 496.下一个更大元素 I 、503.下一个更大元素II
算法
Beau_Will23 分钟前
ZISUOJ 2024算法基础公选课练习一(2)
算法
XuanRanDev25 分钟前
【每日一题】LeetCode - 三数之和
数据结构·算法·leetcode·1024程序员节
gkdpjj27 分钟前
C++优选算法十 哈希表
c++·算法·散列表