BFS——广度优先算法(Breadth First Search)-程序员宅基地

技术标签: 数据结构与算法  

1、前言

这几天刷leetcode经常碰到DFS BFS的问题,之前一直也是模棱两可,凭着感觉做,是需要总结一下了。

广度优先搜索(也称宽度优先搜索,缩写BFS,以下采用广度来描述)是连通图的一种遍历策略。因为它的思想是从一个顶点V0V0开始,辐射状地优先遍历其周围较广的区域,因此得名。

一般可以用它做什么呢?一个最直观经典的例子就是走迷宫,我们从起点开始,找出到终点的最短路程,很多最短路径算法就是基于广度优先的思想成立的。

BFS的理论证明这里不作重点,有需要的童鞋可以自行查阅相关书籍,本博客主要以几个例子说明一下BFS算法。

2、BFS的基本思路

连通图示例图 
上图是连通图,一般把顶点用ViVi表示,边用EijEij表示。

2.1 算法的基本思路

常常我们有这样一个问题:从一个起点开始要到一个终点,我们要找寻一条最短的路径。以下图为例,如果我们要求V0到V6的一条最短路(假设走一个节点按一步来算),我们明显看出这条路径就是V0>V2>V6V0−>V2−>V6,而不是V0>V3>V5>V6V0−>V3−>V5−>V6。先想想你自己刚刚是怎么找到这条路径的:首先看跟V0V0直接连接的节点V1V2V3V1、V2、V3,发现没有V6V6,进而再看刚刚V1V2V3V1、V2、V3的直接连接节点分别是:{ V0V4V0、V4}、{ V0V1V6V0、V1、V6}、{ V0V1V5V0、V1、V5}(这里画删除线的意思是那些顶点在我们刚刚的搜索过程中已经找过了,我们不需要重新回头再看他们了)。这时候我们从V2V2的连通节点集中找到了V6V6,那说明我们找到了这条V0到V6的最短路径:V0>V2>V6V0−>V2−>V6,虽然你再进一步搜索V5的连接节点集合后会找到另一条路径V0>V3>V5>V6V0−>V3−>V5−>V6,但显然他不是最短路径,我们将其扔掉。

我们采用示例图来说明这个过程,在搜索的过程中,初始所有节点是白色代表了所有点都还没开始搜索),把起点V0V0标志成灰色(表示即将辐射V0V0),下一步搜索的时候,我们把所有的灰色节点访问一次,然后将其变成黑色表示已经被辐射过了),进而再将他们所能到达的节点标志成灰色(因为那些节点是下一步搜索的目标点了),但是这里有个判断,就像刚刚的例子,当访问到V1V1节点的时候,它的下一个节点应该是V0V0V4V4,但是V0V0已经在前面被染成黑色了,所以不会将它染灰色。这样持续下去,直到目标节点V6V6被染灰色,说明了下一步就到终点了,没必要再搜索(染色)其他节点了,此时可以结束搜索了,整个搜索就结束了。然后根据搜索过程,反过来把最短路径找出来,下图中把最终路径上的节点标志成绿色

整个过程如下图所示: 
这里写图片描述 
初始全部都是白色(未访问) 
这里写图片描述 
即将搜索起点V0(灰色) 
这里写图片描述 
已搜索V0,即将搜索V1、V2、V3 
这里写图片描述 
终点V6被染灰色,终止 
这里写图片描述 
找到最短路径

2.2 广度优先搜索流程图

这里写图片描述

3、BFS的核心代码

/** 
 * 广度优先搜索 
 * @param Vs 起点 
 * @param Vd 终点 
 */  
bool BFS(Node& Vs, Node& Vd){  
    queue<Node> Q;  
    Node Vn, Vw;  
    int i;  

    //初始状态将起点放进队列Q  
    Q.push(Vs);  
    hash(Vw) = true;//设置节点已经访问过了!  

    while (!Q.empty()){
   //队列不为空,继续搜索!  
        //取出队列的头Vn  
        Vn = Q.front();  

        //从队列中移除  
        Q.pop();  

        while(Vw = Vn通过某规则能够到达的节点){  
            if (Vw == Vd){
   //找到终点了!  
                //把路径记录,这里没给出解法  
                return true;//返回  
            }  

            if (isValid(Vw) && !visit[Vw]){  
                //Vw是一个合法的节点并且为白色节点  
                Q.push(Vw);//加入队列Q  
                hash(Vw) = true;//设置节点颜色  
            }  
        }  
    }  
    return false;//无解  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

对于一个题目来说,要标志节点是否访问过,用数组是一种很快速的方法,但有时数据量太大,很难用一个大数组来记录时,采用hash set是最好的做法。实际上visit数组在这里也是充当hash set的作用。

4、例1:Word Ladder (Leetcode 127 Medium)

这里写图片描述 
问题描述:根据给定单词,找出可以转换的最短距离 
要求: 
①每次只能变换一个字母 
②每个字符串长度相同 
③每次转换的字符串必须在wordList中 
④如果没有这种转换,返回0 
⑤没有重复的 
思路说明:

  • ① 构造一个字典,key为”ot”,”h_t”,”ho“这样的形式,即可把满足只变化一个字母的单词归并到同一个key的value中了。
  • ② 运用BFS来做
class Solution(object):
    def ladderLength(self, beginWord, endWord, wordList):
    # beginWord为开始的单词,endWord为目标单词,wordList为可供转换的元素组成的列表
        def construct_dict():
            d = {}
            for word in wordList:
                for i in xrange(len(word)):
                    s = s[:i] + "_" + s[i+1:]
                    d[s] = d.get(s, []) + [word]
            return d

        def bfs(begin, target, d):
            queue, visited = [(begin, 1)], set()
            while len(queue) > 0:
                word, steps = queue.pop(0)
                if word not in visited:
                    visited.add(word)
                    if word == target:
                        return steps
                    for i in xrange(len(word)):
                        s = word[:i] + "_" + word[i+1:]
                        for j in d.get(s, []):
                            # 如果j在visited中,说明j已经被访问过了,不需要再次进行访问。
                            if j not in visited:
                                queue.append((j, steps+1))
            return 0

        d = construct_dict()
        return bfs(beginWord, endWord, d)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

5、例2:Word Break II (Leetcode 140 Hard)

这里写图片描述 
问题描述:给定一个字符串和一个字典,判断字典是否包含可以构成这个字符串的元素,如果有,返回由这些单词组成的字符串,以列表形式表示。

思路说明:

  • 这道题特别好,考察了BFS和DFS的联合使用,下面给出我的思路,这道题还有很多可以优化的地方,但是主要是思路。

基于BFS的主要想法是:图的顶点ViVi可以用每个单词的第一个字母所在的索引来表示,图的边EijEij则用单词表示。 
以nightmare为例,比如我们划分成了night mare,那么图应该为 
0 —> 5 —> 9。 
于是问题转换成了,检查是否有一条路径从0到9(并列出所有的可能,需要用到DFS了)

class Solution(object):
    def wordBreak(self, s, wordDict):
    """
        s 目标字符串
        wordDict 存放可能组成s的元素组成的列表
        返回值 列表List
    """
        self.val = []
        self.dfs(s, wordDict, "")
        return self.val

    def dfs(self, s, wordDict, ret):
        if len(s) == 0:
            ret = ret.strip()
            ret = ret.split()[::-1]
            ret = reduce(lambda x,y: x + " " + y,ret, "")
            self.val.append(ret.strip())

        else:
            bfs = [] # 队列
            visited = set() # 访问过的点,访问过的不再访问
            bfs.append(0) # 队列初始化
            while len(bfs) > 0:
                start = bfs.pop(0)
                if start not in visited:
                    visited.add(start)
                    for j in xrange(start+1, len(s)+1):
                        word = s[start:j] 
                        if word in wordDict:
                            bfs.append(j)
                            if j == len(s):
                                self.dfs(s[:start], wordDict, ret + " " + word)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

6、总结

假设图有V个顶点,E条边,广度优先搜索算法需要搜索V个节点,因此这里的消耗是O(V),在搜索过程中,又需要根据边来增加队列的长度,于是这里需要消耗O(E),总得来说,效率大约是O(V+E)。 
其实最影响BFS算法的是在于Hash运算,我们前面给出了一个visit数组,已经算是最快的Hash了,但有些题目来说可能Hash的速度要退化到O(lgn)的复杂度,当然了,具体还是看实际情况的。 
BFS适合此类题目:给定初始状态跟目标状态,要求从初始状态到目标状态的最短路径。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hellozhxy/article/details/80065091

智能推荐

JWT(Json Web Token)实现无状态登录_无状态token登录-程序员宅基地

文章浏览阅读685次。1.1.什么是有状态?有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。缺点是什么?服务端保存大量数据,增加服务端压力 服务端保存用户状态,无法进行水平扩展 客户端请求依赖服务.._无状态token登录

SDUT OJ逆置正整数-程序员宅基地

文章浏览阅读293次。SDUT OnlineJudge#include<iostream>using namespace std;int main(){int a,b,c,d;cin>>a;b=a%10;c=a/10%10;d=a/100%10;int key[3];key[0]=b;key[1]=c;key[2]=d;for(int i = 0;i<3;i++){ if(key[i]!=0) { cout<<key[i.

年终奖盲区_年终奖盲区表-程序员宅基地

文章浏览阅读2.2k次。年终奖采用的平均每月的收入来评定缴税级数的,速算扣除数也按照月份计算出来,但是最终减去的也是一个月的速算扣除数。为什么这么做呢,这样的收的税更多啊,年终也是一个月的收入,凭什么减去12*速算扣除数了?这个霸道(不要脸)的说法,我们只能合理避免的这些跨级的区域了,那具体是那些区域呢?可以参考下面的表格:年终奖一列标红的一对便是盲区的上下线,发放年终奖的数额一定一定要避免这个区域,不然公司多花了钱..._年终奖盲区表

matlab 提取struct结构体中某个字段所有变量的值_matlab读取struct类型数据中的值-程序员宅基地

文章浏览阅读7.5k次,点赞5次,收藏19次。matlab结构体struct字段变量值提取_matlab读取struct类型数据中的值

Android fragment的用法_android reader fragment-程序员宅基地

文章浏览阅读4.8k次。1,什么情况下使用fragment通常用来作为一个activity的用户界面的一部分例如, 一个新闻应用可以在屏幕左侧使用一个fragment来展示一个文章的列表,然后在屏幕右侧使用另一个fragment来展示一篇文章 – 2个fragment并排显示在相同的一个activity中,并且每一个fragment拥有它自己的一套生命周期回调方法,并且处理它们自己的用户输_android reader fragment

FFT of waveIn audio signals-程序员宅基地

文章浏览阅读2.8k次。FFT of waveIn audio signalsBy Aqiruse An article on using the Fast Fourier Transform on audio signals. IntroductionThe Fast Fourier Transform (FFT) allows users to view the spectrum content of _fft of wavein audio signals

随便推点

Awesome Mac:收集的非常全面好用的Mac应用程序、软件以及工具_awesomemac-程序员宅基地

文章浏览阅读5.9k次。https://jaywcjlove.github.io/awesome-mac/ 这个仓库主要是收集非常好用的Mac应用程序、软件以及工具,主要面向开发者和设计师。有这个想法是因为我最近发了一篇较为火爆的涨粉儿微信公众号文章《工具武装的前端开发工程师》,于是建了这么一个仓库,持续更新作为补充,搜集更多好用的软件工具。请Star、Pull Request或者使劲搓它 issu_awesomemac

java前端技术---jquery基础详解_简介java中jquery技术-程序员宅基地

文章浏览阅读616次。一.jquery简介 jQuery是一个快速的,简洁的javaScript库,使用户能更方便地处理HTML documents、events、实现动画效果,并且方便地为网站提供AJAX交互 jQuery 的功能概括1、html 的元素选取2、html的元素操作3、html dom遍历和修改4、js特效和动画效果5、css操作6、html事件操作7、ajax_简介java中jquery技术

Ant Design Table换滚动条的样式_ant design ::-webkit-scrollbar-corner-程序员宅基地

文章浏览阅读1.6w次,点赞5次,收藏19次。我修改的是表格的固定列滚动而产生的滚动条引用Table的组件的css文件中加入下面的样式:.ant-table-body{ &amp;amp;::-webkit-scrollbar { height: 5px; } &amp;amp;::-webkit-scrollbar-thumb { border-radius: 5px; -webkit-box..._ant design ::-webkit-scrollbar-corner

javaWeb毕设分享 健身俱乐部会员管理系统【源码+论文】-程序员宅基地

文章浏览阅读269次。基于JSP的健身俱乐部会员管理系统项目分享:见文末!

论文开题报告怎么写?_开题报告研究难点-程序员宅基地

文章浏览阅读1.8k次,点赞2次,收藏15次。同学们,是不是又到了一年一度写开题报告的时候呀?是不是还在为不知道论文的开题报告怎么写而苦恼?Take it easy!我带着倾尽我所有开题报告写作经验总结出来的最强保姆级开题报告解说来啦,一定让你脱胎换骨,顺利拿下开题报告这个高塔,你确定还不赶快点赞收藏学起来吗?_开题报告研究难点

原生JS 与 VUE获取父级、子级、兄弟节点的方法 及一些DOM对象的获取_获取子节点的路径 vue-程序员宅基地

文章浏览阅读6k次,点赞4次,收藏17次。原生先获取对象var a = document.getElementById("dom");vue先添加ref <div class="" ref="divBox">获取对象let a = this.$refs.divBox获取父、子、兄弟节点方法var b = a.childNodes; 获取a的全部子节点 var c = a.parentNode; 获取a的父节点var d = a.nextSbiling; 获取a的下一个兄弟节点 var e = a.previ_获取子节点的路径 vue