【算法入门】数十道题总结的经验,看完本篇学会广度优先搜索BFS,超详细,建议收藏

32 篇文章 11 订阅
18 篇文章 7 订阅

✂️✂️✂️


BFS概述

上一章【算法入门】最短时间学会DFS深度优先搜索中到了DFS和BFS的差异,其中我们能够看到BFS这种遍历方式对于迷宫问题的求解其实是非常合适的,接下来先带看看最基本迷宫问题当中如何使用BFS来实现在这里插入图片描述

预备知识:学习BFS,用BFS走迷宫

在这里插入图片描述
💡💡💡💡💡
类似这种我们从起点走到终点,我们BFS通常是使用队列实现的,我们将一开始的起点入队列之后,我们从队列取出所有结点,再将他们的上下左右结点带入队列,这样就可以实现BFS深度遍历了!!
示意图:三种情况,图中有标识注意的三点在这里插入图片描述

分析上图:所以我们就可以看出,当我们要走这个迷宫的时候,要处理第一种,我们要提前判断坐标是否在我们开的数组当中。第二种,我们遇到障碍物就不用入队列的操作,第三种:走过的点不能再走,我们就弄一个标记矩阵(vector<vector>visited)来标识我们走过的点,对于走过的点也不入队列,原因:当我们不将新元素入队列说明这个点已经走到头了

提示:这里的坐标我们选择用node进行存储,这里实现的是是否能从起点 到终点的接口,大家可以复制下面代码到自己的编辑器下跑一跑,下面是我测试的图:

//grid为0表示无障碍物,为1表示有障碍物
		vector<vector<int>> grid
		{ {0,0,1,1,1}
		 ,{0,0,0,1,0}
		 ,{0,0,0,1,0}
		 ,{0,1,1,1,1}
		 ,{0,0,0,0,0} };

在这里插入图片描述

#include<queue>
#include<stdbool.h>
int arr[4][2] = { {1,0},{-1,0},{0,-1},{0,1} };
struct node
{
	//对于结点存储的坐标进行初始化
	node(int x, int y)
		:_x(x),
		_y(y)
	{}

	int _x;
	int _y;
};
//能够到返回1,不能返回0
bool BFS(vector<vector<int>>& visited, vector<vector<int>> grid, int startx, int starty,int destx,int desty, int row, int col)
{
	//如果起点和终点是同一个点直接返回
	if (startx == destx && starty == desty)
		return true;
	//我们先对起点进行入队列操作
	//这里照顾java的同学所以使用node存储结点,这里用pair存储结点也是一样的
	queue<node> q;
	q.push(node(startx, starty));
	//并且将已经入队列的点进行初始化
	visited[startx][starty] = 1;
	while (!q.empty())
	{
		//入当前队列所有结点的上下左右坐标,并将队列开始时有的结点出出去
		int sz = q.size();
		//将所有队列当中的元素删除,并入上他们周围四个点的坐标
		for (int i = 0; i < sz; ++i)
		{
			//将当队头元素的周围四个坐标入队列,再删除队头元素
			node tmp = q.front();
			q.pop();
			//处理当前点的周围四个点
			for (int j = 0; j < 4; ++j)
			{
				//方向矩阵,不知道的请看看上一章的DFS讲解
				int newX = tmp._x + arr[j][0];
				int newY = tmp._y + arr[j][1];
				//坐标越界,不合法,过滤
				if (newX < 0 || newX >= row || newY < 0 || newY >= col)
					continue;
				//没有访问过并且不是障碍物的点入队列
				if (visited[newX][newY] == 0 && grid[newX][newY] == 0)
				{
				//如果入的点是我们的终点我们就返回true,注意倘若终点若是障碍物,我们也走不到这里,最终也会返回false,走不到
					if (newX == destx && newY == desty)
						return true;
					//入的点不是就进行,入队列并且对点做标记
					q.push(node(newX, newY));
					visited[newX][newY] = 1;
				}
			}
		}
	}
	//队列出完了都没有找到(destx,desty)表示这个点访问不到
	return false;

}
//测试函数
int main()
{	
	while (1)
	{
	//实现的是一个输入起点和终点,返回是否能否到的迷宫
		int n = 5;
		cout << "请输入起点坐标 sx,sy  以及终点坐标 dx,dy\n";
		int startx, starty, destx, desty;
		cin >> startx >> starty >> destx >> desty;
		//设置我们grid当中的障碍物
		//grid为0表示无障碍物,为1表示有障碍物
		vector<vector<int>> grid
		{ {0,0,1,1,1}
		 ,{0,0,0,1,0}
		 ,{0,0,0,1,0}
		 ,{0,1,1,1,1}
		 ,{0,0,0,0,0} };
		//初始化标记矩阵
		vector<vector<int>> visited(n, vector<int>(n, 0));
	
		cout<<BFS(visited, grid, startx, starty, destx, desty, n, n);
	}
	return 0;
}

看完上面我们就可以对BFS做一个小总结

BFS()
{
	1.初始步骤,初始化队列(将首元素入队列)
	2.遍历队列中的每一种可能,while(队列不为空)
	{
		通过当前的队头元素带出下一步的可能,依次入队列
		{
			判断是否满足,按照要求进行逻辑处理
		}
		继续遍历队列中的剩余情况
	}
}


讲到这里听的懂的老哥就可以走到我们的下一步了,做几道练习题来实验实验吧。


习题一: 员工的重要性

📝📝📝
员工的重要性
看过我写的DFS的老哥就知道这题之前写过,现在我们再尝试用BFS的方式写写这道题目
在这里插入图片描述

	/*
// Definition for Employee.
class Employee {
public:
    int id;
    int importance;
    vector<int> subordinates;
};
*/

class Solution {
public:
    int getImportance(vector<Employee*> employees, int id) {
        
    }
};

思路:我们要求出当前id的这个人的下属以及他的下属的重要度,所以我们可以用一个queue<Employee*>用来要求的id的结构体指针,这样子我们就可以通过访问他的subordinates,依次将他的下属入队列,将所有的重要度都加起来返回就可以了,因为这里我们有用到id 找到对应雇员的结构体指针,所以我们用个哈希表(um)来帮助我们查找


class Solution {
public:
int BFS(unordered_map<int,Employee*>& um,int id)
{
    //初始化队列
    queue<Employee*> q;
    q.push(um[id]);
    int importance = 0;
    //遍历队列
    while(!q.empty())
    {
        //将当前队列中的每一个id对应的雇员的重要度相加,将他们手下的雇员入队列
        //重复这个过程
        int sz = q.size();
        //计算当前队列中的数据,并且依次取出
        for(int i =0;i<sz;++i)
        {
            //每次取队头数据
            Employee* front = q.front();
            q.pop();
            importance+=front->importance; 
            //再将出掉的队头数据的直系下属入队
            for(int subid: front->subordinates)
            {
                q.push(um[subid]);
            }
        }
    }
    return importance;
}
    int getImportance(vector<Employee*> employees, int id) {
        unordered_map<int,Employee*> um;
        //哈希表建立id与雇员的映射
        for(Employee* e: employees)
        {
            um[e->id] = e;
        }
        //这里我们可以写一个接口
        return BFS(um,id);
    }
};

没看懂的同学可以再看看这张图,应该就会比较清晰了
第一步:
在这里插入图片描述
第二步: 体现for循环的作用
在这里插入图片描述
在这里插入图片描述
看完这里的你,理解完第一题,已经就成功了一大半了,让我们继续把接下来的题干完吧!!!


习题二: N叉树的层序遍历

📝📝📝
429. N 叉树的层序遍历

在这里插入图片描述
大家如果学习过二叉树的话想比都写过:二叉树的层序遍历这样的题,实际上N叉树也是大同小异的,图解:
在这里插入图片描述

// Definition for a Node.
class Node {
public:
    int val;
    vector<Node*> children;

    Node() {}

    Node(int _val) {
        val = _val;
    }

    Node(int _val, vector<Node*> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Solution {
public:
    vector<vector<int>> levelOrder(Node* root) {
        
    }
};

分析:他这里给我们二叉树的头,并且我们可以通过vector<Node*> children,表示我们可以拿到当前头节点指针他所有的孩子,他要求我们返回的是二维数组,那我们这题也试试用BFS来解决

class Solution {
public:
    vector<vector<int>> levelOrder(Node* root) {
        if(root==NULL)
        return vector<vector<int>>();
        //初始化队列
        queue<Node*> q;
        q.push(root);
        //用于返回的retvv
        vector<vector<int>> retvv;
        while(!q.empty())
        {
            //计算每一层的元素大小
            int sz =q.size();
            //存储每一行的结果
            vector<int> v; 
            for(int i =0;i<sz;++i)
            {
                Node* front = q.front();
                q.pop();
                v.push_back(front->val);

                //vector<Node*> children;
                //将下一层的数据全部入队列
                for(Node* child: front->children)
                {
                    q.push(child);
                }
            }
            retvv.push_back(v);
        }
        return retvv;
    }
};

这道题和上一道并没有什么新的知识点,我们直接转到下一道题

在这里插入图片描述


习题四: 腐烂的橘子

📝📝📝
994. 腐烂的橘子
题目:在这里插入图片描述

这里我简单的画了一下每分钟分别是哪些橘子腐坏:

在这里插入图片描述

这道题目就有点意思了,他要求我们求出没有新鲜橘子为止所必须经过的最小分钟数,在这种求最小值的场景当中,BFS是很有优势的,它能够遍历所有可能返回最小值的结果,这题是可能有若干个腐烂的橘子,同一分钟它们都会腐烂他们的上下左右的橘子

注意:这题目不是很难,但是结合了之前所学的知识,标记矩阵不会的可以看看【算法入门】最短时间学会DFS深度优先搜索,以及队列初始化不是一定只入一个值,也并不是什么时候都是需要标记矩阵的,像这里的腐烂橘子本身就是一种标记,这道题目挺好的,值得动手写写!

int arr[4][2] ={ { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
class Solution {
public:
        int orangesRotting(vector<vector<int>>& grid) {
        if(grid.empty())
        return 0;

        //初始化队列:因为腐烂橘子可能不止一个,我们可以遍历这个二维数组先将所有腐烂的橘子入队列
        queue<pair<int,int>> q;
        int clock = 0;//记录时间

        //记录是否有新鲜橘子
        int flag =0;
        int row =grid.size();
        int col=grid[0].size();
        for(int i=0;i<row;++i)
        {
            for(int j =0;j<col;++j)
            {
                //腐烂橘子为2
                if(grid[i][j]==2)
                q.push(make_pair(i,j));
                if(grid[i][j]==1)
                flag=1;
            }
        }
        //这里1.如果没有数据入队列的话,就说明没有腐烂橘子,此时分两种情况,2.没有橘子和存在新鲜橘子
        //1.存在新鲜橘子并且没法腐烂
        if(q.empty() && flag==1)
        return -1;
        //2.没有新鲜橘子并且没有腐烂橘子
        else if(q.empty()&& flag==0)
        return 0;

        //此时q当中存放着所有的腐烂橘子,他们在同一时间腐烂周围的橘子
        //遍历队列当中所有可能
        while(!q.empty())
        {
            //先前的题一开始sz都是1,这题就不一定是了
            int sz =q.size();
            
            //遍历队列当中的所有腐烂橘子 ,遍历一遍队列只过了一分钟!
            for(int i =0;i<sz;++i)
            {
                pair<int,int> first = q.front();
                q.pop();

                //遍历队头元素他的周围的坐标,方向矩阵
                for(int j =0;j<4;++j)
                {
                    int newx =first.first+arr[j][0];
                    int newy =first.second+arr[j][1];
                    //判断坐标是否合法
                    if(newx<0||newx>=row||newy<0||newy>=col)
                    continue;
                    //如果是新鲜橘子,将新鲜橘子的变成腐烂橘子,并且入队
                    if(grid[newx][newy]==1)
                    {
                        //这里相当于对于新鲜橘子做了标记,就不需要标记矩阵了
                        grid[newx][newy] = 2;
                        q.push(make_pair(newx,newy));
                    }
                }
            }
            clock++;
        }
        //走到这里,两种情况
        //情况一:剩余新鲜橘子没有被感染
        for(int i=0;i<row;++i)
        {
            for(int j =0;j<col;++j)
            {
                //若存在新鲜橘子,返回-1(题目要求)
                if(grid[i][j]==1)
                return -1;
            }
        }
        //情况二:全部被感染,返回时间-1,因为最后一个新鲜橘子腐烂入队列后已经没有新鲜橘子需要感染了,但是clock也会++,所以这里要减一次!!这里也可以在前面判断,都一样。
        return clock-1;
    }
};

这道题之前,希望你不要轻易放弃,只要这题没问题,基本上算法入门了!
在这里插入图片描述

习题五: 单词接龙(较难)

📝📝📝
127. 单词接龙
在这里插入图片描述
在这里插入图片描述

这道题的难度虽然是困难,但是我们稍加分析,它只是我们之前几道题的结合。
分析:我们可以把beginWord先入队列,再将队列中的单词只做一个单词的变化查看是否能在dict(我所设置的哈希表)中查找到,如果可以,就入队列,每次将队列所有的都修改一次,并记录count(修改的次数),直到修改一个词后等于endWord我们就可以返回次数,如果队列遍历完,从队列中没有找到一个单词修改一个字母就能到达结果的,就表示没可能到这个词了。
在这里插入图片描述
这张图其实就能很好的表示每次只修改一个词,到最终与endWord相等。

图解在这里插入图片描述


5.1使用哈希表的原因

📝📝📝
注意vector< string > & wordList,只能用< algorithm >这个库中的查找,效率是O(N),所以我们放入unordered_set< string > dict中查找,效率是O(1),所以我们会进行如下操作在这里插入图片描述

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        unordered_set<string> dict;
        for(string& e: wordList)
        {
            dict.insert(e);
        }
        //因为修改单词的时候可能出现 dog -- cog,可能修改来修改去都在这两个走,所以已经修改成的单词我们都标记一下
        unordered_map<string,int> visited;//默认visited当中的单词第二个参数都是0
        //当最终endWord都不在wordlist当中,肯定找不到,返回0
        if(dict.find(endWord) == dict.end())
        return 0;

        //这道题我们可以将beginWord入队列做初始化
        queue<string> q;
        q.push(beginWord);
        visited[beginWord] = 1;

        //记录修改次数,一开始也是算在里面的
        int count=1;

        //遍历队列当中的所有可能
        while(!q.empty())
        {
            int sz =q.size();

            //遍历队列中的每一个词
            for(int i =0;i < sz;++i)
            {
                string s =q.front();
                q.pop();
                //s的长度
                int len = s.size();
                //对于队列中的每一个单词,我们转换单词每一个字符为其他25个字符,看看是否存在在dict字典当中 
                for(int j =0;j < len;++j)
                {
                    for(int k = 'a';k <='z';++k)
                    {
                        //每次改变都要在s的基础上改变,所以我们要创建临时变量tmp
                        string tmp = s;

                        if(tmp[j] != k)
                        {
                            tmp[j] = k;

                            //变换一个单词之后检测是否能在dict中查找到
                            if(tmp == endWord)
                            {
                                //表示转换了之后就可以在dict当中找到了,只要是转换得来的都会在这个地方返回
                                return count+1;
                            }
                            if(dict.find(tmp) != dict.end()
                            && visited[tmp] == 0)
                            {
                                cout<<tmp<<" ";
                                //如果修改的词在dict当中能查找到,就入队列
                                q.push(tmp);
                                //并且标记成1,以免下次进来又转换为这个单词
                                visited[tmp] =1;
                            }
                        }
                        else
                        continue;     
                    }
                }
            }
            count++;//遍历完队列当中的所有单词表示修改了一次       
        }
        //转换得不到结果
        return 0;
    }
};

习题六:最小基因变化

📝📝📝
433. 最小基因变化
在这里插入图片描述
📝📝📝

思想是和之前差不多的,上题没问题的话,这题就是送分题
这里可以用个全局arr来存储我们要的ACGT,方便后续的代码使用

char arr[4]={'A','C','G','T'};
class Solution {
public:
    int minMutation(string start, string end, vector<string>& bank) {
    //库当中<algorithm>提供的查找:InputIterator find (InputIterator first, InputIterator last, const T& val);
        //end不存在bank就不用再往下走了
        if(find(bank.begin(),bank.end(),end) == bank.end())
        return -1;

        //记录次数
        int count =0;

        queue<string> q;
        q.push(start);
        
        unordered_set<string> genebank;
        //这题和上题一样都需要哈希表标记,和哈希表查找
        for(string& s: bank)
        {
            genebank.insert(s);
        }
        //标记
        unordered_map<string,int> visited;
        visited[start] = 1;
        while(!q.empty())
        {
            int sz =q.size();
            //每次将当前队列所有的元素遍历
            for(int i =0;i<sz;++i)
            {
                //将一个单词进行修改,再和genebank基因库里的比较
                string front = q.front();
                q.pop();
                
                //记录当前单词的长度
                int len =front.size();
                //对该单词的任一个字符进行修改
                for(int j=0;j<len;++j)
                {
                    string tmp =front;
                    for(int k =0;k<4;++k)
                    {
                        //进行一个字符的修改,这里用一个全局的arr用来保存修改字符的所有可能(4种)
                        if(tmp[j]!=arr[k])
                        tmp[j] = arr[k];
                        else
                        continue; // 表示修改过后和原单词相同,我们这里跳过就可以了

                        //修改后判断是否为我们最终单词
                        if(tmp == end)
                        return count+1;

                        //判断是否在基因库当中genebank
                        if(genebank.find(tmp) !=genebank.end() &&visited[tmp] == 0)
                        {
                            //入队列,做标记
                            q.push(tmp);
                            visited[tmp] =1;
                        }
                    }
                }
            }
            count++;
        }
        return -1;//无法实现
    }
};

习题七: 打开转盘锁

📝📝📝
752. 打开转盘锁
在这里插入图片描述
在这里插入图片描述

注意这道题每个数字都只能往上或者往下拨动一次,还有要处理当字符’(’0‘-1),(’9‘+1)这两种不正确的数字。

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
//这道题目就是我们如果能够在不把deadends当中的string入队列就能进行从“0000” 到target的变换就输出次数
//不然就输出-1
        //"0000"在死亡数字当中
        if(find(deadends.begin(),deadends.end(),string("0000"))!=deadends.end())
        return -1;
        if(target == string("0000"))
        return 0;
        
        //用哈希set便于查找
        unordered_set<string> deadword;
        for(string& s: deadends)
        {
            deadword.insert(s);
        }

        unordered_map<string,int> visited;
        
        
        //记录修改次数
        int count =0;

        //这题也需要标记矩阵避免来回挑拨数字
        queue<string> q;
        q.push(string("0000"));
        //对初始进行标记
        visited[q.front()] = 1;

        while(!q.empty())
        {
            int sz =q.size();
            //将队列中的每一个单词做一次改变
            for(int i =0;i<sz;++i)
            {
                string front = q.front();
                q.pop();

                //记录当前单词的长度
                int len =front.size();
                //对一个单词进行调整
                for(int j=0;j<len;++j)
                {
                    string tmp =front;
                    //往上调整
                    int upNum = tmp[j]+1;
                    if(upNum == (10 +'0'))
                    upNum = '0';

                    //往下调整
                    int downNum = tmp[j]-1;
                    if(downNum == ('0'-1) )
                    downNum = '9';

                    //对tmp单词进行修改

                    //1.向上调整
                    tmp[j] = upNum;
                    if(target == tmp)
                    return count +1;
                    //死亡列表找不到并且之前没有入过队列
                    else if(deadword.find(tmp) == deadword.end() && visited[tmp] ==0 )
                    {
                        //在死亡列表中找不到,就入队列,做标记
                        q.push(tmp);
                        visited[tmp] =1;
                    }
                    
                    //2.向下调整
                    tmp[j] = downNum;
                    if(target == tmp)
                    return count +1;
                    else if(deadword.find(tmp) == deadword.end() && visited[tmp] ==0 )
                    {
                        //在死亡列表中找不到,就入队列,做标记
                        q.push(tmp);
                        visited[tmp] =1;
                    }
                }
            }
            count++;
        }
        //怎么修改都修改不到
        return -1;
    }
};

🔑 🔑 🔑

总结

这节讲述的BFS其实核心在一开始就已经说完了,对于求最小值的时候可以拿出来使用,但是习题是多样的,只有多练习才能够越来越理解他的思想,如果上述有什么表述不对的,欢迎来指点,
下节会讲回溯思想和著名的N皇后问题!让我们拭目以待。
如果可以的话,可以给一个小小的三连吗❤❤❤

  • 31
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

^jhao^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值