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

目录

一.引言

二.双向 BFS 简介

1.双向遍历示例

2.搜索模版回顾

三.经典算法实战

1.Word-Ladder [127]

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

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 

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 + 剪枝

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

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

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* 启发式搜索。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/308119.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

动手学深度学习-卷积神经网络

卷积神经网络 在前面的章节中,我们遇到过图像数据。这种数据的每个样本都由一个二维像素网格组成,每个像素可能是一个或者多个数值,取决于是黑白还是彩色图像。到目前为止,我们处理这类结构丰富的数据方式还不够有效。我们仅仅通…

【web缓存】nginx和CDN应用

目录 一、代理的工作机制 二、代理服务器的概念 三、代理服务器的作用 四、常用的代理服务器 五、nginx缓存代理部署 步骤一:首先脚本完成三台nginx的部署 步骤二:在两个后端原始服务器上分别创建测试页面 步骤三:完成nginx缓存服务器…

RedisTemplate详解

一、SpringDataRedis简单介绍及引入 SpringData是Spring中数据操作的模块,包括对各种数据库的集成,其中对Redis的集成模块就叫SpringDataRedis 官网地址:https://spring.io/projects/spring-data-redis 1.1 特点: 提供了对不同…

观成科技-加密C2框架EvilOSX流量分析

工具简介 EvilOSX是一款开源的,由python编写专门为macOS系统设计的C2工具,该工具可以利用自身释放的木马来实现一系列集成功能,如键盘记录、文件捕获、浏览器历史记录爬取、截屏等。EvilOSX主要使用HTTP协议进行通信,通信内容为特…

公司新来的同事给出了if-else优化的8种方案

我们日常开发的项目中,如果代码中存在大量的if-else语句,阅读起来非常的折磨(直接劝退),维护起来也很难,也特别容易出问题。比如说以下: 接下来,本文介绍我们常使用的8种方法去优化…

xinput1_4.dll缺失了怎么办?快速修复xinput1_4.dll文件的方法指南

在快速发展的数字时代,电子设备尤其是电脑成为了我们生活工作中必不可少的工具。然而,在使用过程中,我们可能会遇到各式各样的技术问题,其中一个常见问题是系统提示缺少 xinput1_4.dll文件。这个错误通常会在你尝试运行一个游戏或…

EF Core 在实际开发中,如何分层?

前言:什么是分层? 分层就是将 EF Core 放在单独的项目中,其它项目如 Asp.net core webapi 项目引用它这样的好处是解耦和项目职责的清晰划分,并且可以重用 EF Core 项目但是也会数据库迁移变得复杂起来 Step by step 步骤 创建一…

linux 安装 reids并使用Windows测试结果

要安装两个软件 Windows端安装下面的软件连接虚拟机中的redis Another Redis DeskTop Manager 安装和使用_another redis desktop怎么连接-CSDN博客 redis安装 查找可用版本 选择安装最多点赞的一个 安装完成后创建redis容器 docker run -t --name redis -p 6379:6379 -d r…

这6个设计小白学习网站,海量免费学习教程!

划到最后“阅读原文”——领取工具包(超过1000工具,免费素材网站分享和行业报告) Hi,我是胡猛夫~,专注于分享各类价值网站、高效工具! ​更多资源,更多内容,欢迎交流!公…

3d模型显示不出来?3d不显示全模型---模大狮模型网

如果3D模型在显示时不完整或者无法显示,可能有几个原因导致: 缩放问题:检查一下模型的缩放是否正确。有时候模型的缩放比例可能非常大或非常小,导致模型无法正确显示。尝试调整模型的缩放值,使其适合场景。 材质问题&…

主食冻干哪款好?十大放心主食冻干名单推荐

作为养猫的人,我们都知道每天最担心的事情就是如何为心爱的猫咪选择一款高品质的猫粮。我们都希望为猫咪提供最好的营养,让它们健康快乐地成长。然而,近期的一些事件,如百利猫粮生虫和VE主食冻干掰开有虫,让我们不得不…

Django的数据库模型的CharField字段的max_length参数与中文字符数的关系探索(参数max_length的单位是字符个数还是字节数?)

01-清理干净之前的数据库迁移信息 02-根据setting.py中的信息删除掉之前建立的数据库 03-删除之后重新创建数据库 04-models.py中创建数据库模型 from django.db import modelsclass User(models.Model):username models.CharField(max_length4)email models.EmailField(uni…

RabbitMQ安装和快速入门

文章目录 1. RabbitMQ2. 安装RabbitMQ2.1 创建shell文件2.2 编写shell文件2.3 检查rabbitmq状态2.4 设置开机自启动2.5 启动插件2.6 开放端口号2.7 创建用户2.8 登入管理页面 3. SpringBoot中集成RabbitMQ3.1 依赖安装3.2 SpringBoot配置3.3 RabbitMQ的配置类3.4 定义消费者和生…

【Cadence】sprobe的使用

实验目的:通过sprobe测试电路中某个节点的阻抗 这里通过sprobe测试输入阻抗,可以通过port来验证 设置如下: 说明:Z1代表sprobe往left看,Z2代表sprobe往right看 结果如下: 可以看到ZM1I0.Z2 顺便给出了I…

谷歌浏览器打不开设置

场景:谷歌浏览器页面空白,并且点击设置没有反应 忘记我是在哪找的解决方案了,先留个记号在这,方便下次查阅

0x53 区间DP

0x53 区间DP 到目前为止,我们介绍的线性DP一般从初态开始,沿着阶段的扩张向某个方向递推,直至计算出目标状态。区间DP也属于线性DP中的一种,它以“区间长度”作为DP的“阶段”,使用两个坐标(区间的左右端点…

MongoDB查找命令find,让数据返回称心如意

业务系统接入数据库后,每天都有大量的数据写入数据库。面对逐日增加的数据,开发人员或数据分析人员,该如何读取数据,怎样设置条件,从数据库中查询数据? 本文基于mongodb的官方文档,整理出find命…

计算机组成原理-程序查询方式(流程图 演示过程 例题 定时查询 独占查询)

文章目录 总览IO方式简介程序查询方式程序查询方式流程图程序查询方式-例题小结 总览 IO方式简介 每次输一个字,就认为状态完成,CPU就会取走数据寄存器的内容 程序查询方式 此时模拟打印三个字符 假设此时三个字符在主存,CPU先从主存读一…

避免重复扣款:分布式支付系统的幂等性原理与实践

这是《百图解码支付系统设计与实现》专栏系列文章中的第(6)篇。 本文主要讲清楚什么是幂等性原理,在支付系统中的重要应用,业务幂等、全部幂等这些不同的幂等方案选型带来的收益和复杂度权衡,幂等击穿场景及可能的严重…