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

Posted ^jhao^

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法入门数十道题总结的经验,看完本篇学会广度优先搜索BFS,超详细,建议收藏相关的知识,希望对你有一定的参考价值。

✂️✂️✂️


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先入队列,再将队列中的单词只做一个单词的变化查看是否能在

以上是关于算法入门数十道题总结的经验,看完本篇学会广度优先搜索BFS,超详细,建议收藏的主要内容,如果未能解决你的问题,请参考以下文章

(原创)BFS广度优先算法,看完这篇就够了

从梦幻西游学会广度优先搜索和A*算法

算法题——深度优先搜索与广度优先搜索

一文搞懂深度优先搜索广度优先搜索(dfsbfs)

一文搞懂深度优先搜索广度优先搜索(dfsbfs)

常见搜索算法:深度优先和广度优先搜索