广度优先搜索以及多源广度优先搜索 2021-01-24

广度优先搜索以及多源广度优先搜索




前言

         广度优先搜索多用于图和树的遍历,二叉树的层序遍历其实就是一种特殊的广度优先搜索。
         本文先从典型的广度优先搜索开始介绍,之后总结单源点广度优先搜索的应用,最后总结多源点广度优先搜索的基本方法,以及二维网格广度优先搜索的模板。

一、题目描述

主要以如下题目为例(题目来源leetcode)

无脑遍历套模板
1. N叉树的层序遍历
2. 从上到下打印二叉树|
3. 从上到下打印二叉树||
4. 从上到下打印二叉树|||


单源点广度优先搜索的应用
5. 二叉树的堂兄弟节点
6. 填充每一个节点的下一个右侧节点指针


多源点广度优先搜索的应用(多用于二维网格的搜索)
7. 腐烂的橘子
8. 地图分析


全局广度优先搜索(特殊的多源点广度优先搜索,只是应用的场合不同)
9. 水域大小
10. 判断二分图
11. 课程表||

二、方法总结以及代码模板

1.单源点广度优先搜索

    单源点广度优先搜索以一个点为源点,将其优先入队,利用辅助数据结构——队列,在之后实现先进先出顺序的访问。具体伪代码如下:
void bfs(参数1 ...)
{
	queue<T> q;
	q.push(...);
	while(!q,empty())
	{
		//先出队 ,实现先进先出的访问
		T temp=q.front();
		q.pop();
		//在考虑刚刚出队的元素的相邻接点,将其相邻节点入队
		//入队时,如果是二叉树,则只需要考虑左右子节点即可
		//对于图或者n叉树,需要结合存储的方式进行遍历(多用for循环)
		for(...)
			q.push(...)		
		//特别注意,在相邻接点入队时需要根据题目要求做出相应调整
		//如当搜索到的某一个节点符合条件时需要及时返回,结束搜索
	}
}
第二种广度优先遍历遍历的模板:带有层次结构的广度优先搜索
void bfs(参数1 ...)
{
	queue<T> q;
	q.push(...);
	while(!q,empty())
	{
		int size=q.size();
		for(int i=0;i<size;++i)
		{
			//先出队 ,实现先进先出的访问
			T temp=q.front();
			q.pop();
			for(...)
				q.push(...)		
		//特别注意,在相邻接点入队时需要根据题目要求做出相应调整
		//如当搜索到的某一个节点符合条件时需要及时返回,结束搜索
		}
	}
}

1.1 第一类典型的层序遍历:N叉树的层序遍历

这道题只需要无脑的套用上述的伪代码模板即可。直接上代码
class Solution {
public:
    vector<vector<int>> levelOrder(Node* root) 
    {
        vector<vector<int>> v;
        if(!root)
            return v;
        queue<Node*> q;
        q.push(root);
        while(!q.empty())
        {
            vector<int> v1;//用于保存每一层的节点的信息
            int size=q.size();
            for(int i=0;i<size;++i)
            {
                Node* ptr=q.front();
                q.pop();
                v1.push_back(ptr->val);
                //对应上述模板的入队过程
                for(int k=0;k<ptr->children.size();++k)
                    q.push(ptr->children[k]);
            }
            v.push_back(v1);
        }    
        return v;
    }
};

1.2 第二类典型的层序遍历:从上到下打印二叉树|&从上到下打印二叉树||

        二者都属于套用层序遍历模板的题目,不同的是需要注意题目要求。如果要求遍历的结果保持树的层次结构则需要对上述伪代码模板做出改进,即在原来的基础上加入for循环,每一次for循环的作用是清空队列中上一层的元素,并装入下一层元素,使一次for循环遍历的都是同一层的元素,从而使遍历具有层次感。
直接上代码:
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) 
    {
        vector<vector<int>> v;
        if(root==NULL)
            return v;
        queue<TreeNode*> q;
        q.push(root);
        while(!q.empty())
        {
            vector<int> v1;
            int size=q.size();
            for(int i=0;i<size;++i)
            {
                TreeNode* t=q.front();
                q.pop();
                v1.push_back(t->val);    
                if(t->left!=NULL)
                    q.push(t->left);
                if(t->right!=NULL)
                    q.push(t->right);
            }
            v.push_back(v1);
        }
        return v;
    }
};

1.3 第三类层序遍历:

请实现一个函数按照之字形顺序打印二叉树
即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印
第三行再按照从左到右的顺序打印,其他行以此类推。
        只需要在原来模板的基础上维护一个变量,标记当前是奇数层还是偶数 层。从而采取不同的访问策略。另外此题根据需求应该使用双端队列。直接上代码:
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) 
    {
        vector<vector<int>> v;
        if(root==NULL)
            return v;
        deque<TreeNode*> q;
        q.push_back(root);
        int count=1;
        while(!q.empty())
        {
            int size=q.size();
            vector<int> v1;
            for(int i=0;i<size;++i)
            {
                if(count%2==0)
                {
                    TreeNode* ptr=q.back();
                    q.pop_back();
                    //注意从后面取出的,应该从队头插入
     //由于要求插入后左节点在右节点的前面,因此先插入右节点。
     //可以直接想想二叉树的结构进行插入               
                    if(ptr->right) q.push_front(ptr->right);
                    if(ptr->left) q.push_front(ptr->left);
                    v1.push_back(ptr->val);
                }
                else 
                {
                    TreeNode* ptr=q.front();
                    q.pop_front();
                    if(ptr->left) q.push_back(ptr->left);
                    if(ptr->right) q.push_back(ptr->right);
                    v1.push_back(ptr->val);
                }
            }
            count++;
            v.push_back(v1);
        }
        return v;
    }
};

2. 单源点广度优先搜素的应用

       在此仅仅提供思路:
对于第一道题:额外维护两个变量用户标记待查找元素x和y的父节点。之后根据“堂兄弟”的定义进行判断
对于第二道题:在“带有层次结构的广度优先搜索模板”的基础上,每当开始遍历新的一层时,维护一个头指针,之后利用这根指针将一层的节点串起来。贴一个优雅的代码:

class Solution {
public:
    Node* connect(Node* root) 
    {
        if(!root)
            return NULL;
        queue<Node*> q;
        q.push(root);
        Node node;
        while(!q.empty())
        {
            int size=q.size();
            Node* temp=&node;
            for(int i=0;i<size;++i)
            {
                Node* ptr=q.front();
                q.pop();
                //典型的链表操作
                temp->next=ptr;
                temp=temp->next;
                if(ptr->left)
                    q.push(ptr->left);
                if(ptr->right)
                    q.push(ptr->right);
            }
        }
        return root;
    }
};

3. 多源点广度优先搜索

3.1先给出定义

       多源广度优先搜索:从多个起点开始同时搜索,将整个原始搜索域分为目标点和源点。每一个源点都可以开始搜索相应的目标点。

3.2 方法

       解决这类题目最简单的方法就是抽象出来一个超级源点,之后多源点广度优先搜索为单源点广度优先搜素。
       二叉树的遍历相当于只有一个源点——root,因此初始化时队列中相当于就只有一个元素root。而多源点搜索抽象出了一个超级源点O,O点的邻接点就是实际的图中的源点。因此初始化队列时需要将所有符合条件的源点全部入队,达到所有源点同时搜索的效果。


详见链接:
腐烂的橘子——官方讲解

地图分析——官方讲解

       在此给出一个二维网格搜索的比较优雅的模板:以题目“地图分析”为例
class Solution {
public:
	int n,m;//定义成类中的成员,便于不同函数的直接访问
	int a[4][2]={{1,0},{-1,0},{0,1},{0,-1}};//用于标记之后可以移动的四个方向

	//vector<vector<int>> v
    //有些题目中可能维护一个等大的二维网格,先初始化为全零,之后用于标
    //记是否到达过了。
    //初始化是直接调用vector类的函数resize
    //v.resize(n,vector<m,0))即可
    int maxDistance(vector<vector<int>>& grid)
    {//多源广度优先搜索
        int n = grid.size();
        int m = grid[0].size();
        queue<pair<int, int>> q;
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < m; ++j)
                if (grid[i][j] == 1)
                    q.push({ i,j });
        int count = -1;
        if(q.size()==n*m)
            return -1;
        while (!q.empty())
        {
            int size = q.size();
            for (int i = 0; i < size; ++i)
            {
                pair<int, int> cp = q.front();
                q.pop();
                for(int i=0;i<4;++i)
                {
                	int x=cp.first+a[i][0],y=cp.second+a[i][1];
                	if(x>=0&&x<n&&y>=0&&y<m&&grid[x][y]==0)
                	{
                		  q.push({x,y});
                    	  grid[x][y]=1;
                	}
                }
            }
            ++count;
        }
        return count;
    }
};


三. 总结

以上就是广度优先搜索的基本分类,本文仅仅简单介绍了广度优先搜索的基本应用。算法题目多种多样,而广度优先的模板就是应用辅助数据结构——队列,实现先进先访问的需求,即先将源点入队,之后每弹出一个元素都需要考虑该元素的相邻元素是否需要入队。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
寒假,皮皮准备去一些城市旅游。有些城市之间有公路,有些城市之间则没有,如下图。为了节省经费以及方便计划旅程,皮皮希望在出发之前知道任意两个城市之前的最短路程。 1033450-20180623095244077-353646184.png 上图中有4个城市8条公路,公路上的数字表示这条公路的长短。请注意这些公路是单向的。我们现在需要求任意两个城市之间的最短路程,也就是求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。 现在需要一个数据结构来存储图的信息,我们仍然可以用一个4*4的矩阵(二维数组e)来存储。比如1号城市到2号城市的路程为2,则设e[1][2]的值为2。2号城市无法到达4号城市,则设置e[2][4]的值为∞。另外此处约定一个城市自己是到自己的也是0,例如e[1][1]为0,具体如下。 1033450-20180623095252434-1650383278.png 基本要求 现在回到问题:如何求任意两点之间最短路径呢?通过之前的学习我们知道通过深度或广度优先搜索可以求出两点之间的最短路径。所以进行n2遍深度或广度优先搜索,即对每两个点都进行一次深度或广度优先搜索,便可以求得任意两点之间的最短路径。可是还有别的方法:Floyd-Warshall算法、Dijkstra算法等。请分别使用这两种算法求取任意两个城市到达的最短路径。允许通过所有顶点作为中转。
06-06
F-Warshall算法和Dijkstra算法都可以用来求解任意两个点之间的最短路径,但它们的时间复杂度和适用场景有所不同。 Floyd-Warshall算法适用于求解带权有向图中任意两个点之间的最短路径,时间复杂度为 $O(n^3)$。其基本思路是:设 $d_{i,j}$ 表示从节点 $i$ 到节点 $j$ 的最短路径长度,初始时 $d_{i,j}$ 被赋值为 $i$ 到 $j$ 的边的长度,若 $i$ 不能到达 $j$,则 $d_{i,j}$ 被赋值为正无穷大。然后,对于每一对节点 $i,j,k$,如果从 $i$ 到 $k$ 再到 $j$ 的路径比从 $i$ 直接到 $j$ 的路径更短,那么就更新 $d_{i,j}$,即 $d_{i,j}=\min(d_{i,j},d_{i,k}+d_{k,j})$。最终,$d_{i,j}$ 就表示从节点 $i$ 到节点 $j$ 的最短路径长度。 以下是使用Floyd-Warshall算法求解任意两个城市之间最短路径的Java代码: ```java import java.util.*; public class Main { public static void main(String[] args) { final int INF = Integer.MAX_VALUE; Scanner in = new Scanner(System.in); int n = in.nextInt(); // 城市数量 int[][] e = new int[n+1][n+1]; // 存储图的信息 // 读入图的信息 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (i == j) { e[i][j] = 0; // 自己到自己的距离为0 } else { e[i][j] = INF; // 初始距离为无穷大 } } } int m = in.nextInt(); // 公路数量 for (int i = 1; i <= m; i++) { int u = in.nextInt(); int v = in.nextInt(); int w = in.nextInt(); e[u][v] = w; // u到v的距离为w } // Floyd-Warshall算法求解任意两点之间最短路径 for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { e[i][j] = Math.min(e[i][j], e[i][k] + e[k][j]); } } } // 输出任意两个城市之间的最短路径 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (e[i][j] == INF) { System.out.print("∞ "); } else { System.out.print(e[i][j] + " "); } } System.out.println(); } } } ``` 另一种算法是Dijkstra算法,它适用于求解有向图中一个起点到其它所有点的最短路径,时间复杂度为 $O(n^2)$。其基本思路是:设 $d_i$ 表示从起点到节点 $i$ 的最短路径长度,初始时 $d_i$ 被赋值为起点到 $i$ 的边的长度,若起点不能到达 $i$,则 $d_i$ 被赋值为正无穷大。然后,找到当前距离起点最近的节点 $j$,更新所有以 $j$ 为起点的边的终点 $k$ 的距离,即 $d_k=\min(d_k,d_j+w_{j,k})$。然后,找到当前距离起点最近的未访问节点 $j$,继续更新距离,直到所有节点都已访问完毕或当前没有可更新的节点为止。 以下是使用Dijkstra算法求解任意两个城市之间最短路径的Java代码: ```java import java.util.*; public class Main { public static void main(String[] args) { final int INF = Integer.MAX_VALUE; Scanner in = new Scanner(System.in); int n = in.nextInt(); // 城市数量 int[][] e = new int[n+1][n+1]; // 存储图的信息 // 读入图的信息 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (i == j) { e[i][j] = 0; // 自己到自己的距离为0 } else { e[i][j] = INF; // 初始距离为无穷大 } } } int m = in.nextInt(); // 公路数量 for (int i = 1; i <= m; i++) { int u = in.nextInt(); int v = in.nextInt(); int w = in.nextInt(); e[u][v] = w; // u到v的距离为w } // Dijkstra算法求解任意两点之间最短路径 int[] d = new int[n+1]; // 存储起点到各个节点的最短路径 boolean[] vis = new boolean[n+1]; // 标记节点是否已经访问过 Arrays.fill(d, INF); d[1] = 0; // 起点为1号城市 for (int i = 1; i <= n; i++) { int u = 0; // 找到当前距离起点最近的未访问节点 for (int j = 1; j <= n; j++) { if (!vis[j] && (u == 0 || d[j] < d[u])) { u = j; } } // 更新所有以 u 为起点的边的终点 k 的距离 for (int k = 1; k <= n; k++) { if (e[u][k] != INF) { d[k] = Math.min(d[k], d[u] + e[u][k]); } } vis[u] = true; } // 输出任意两个城市之间的最短路径 for (int i = 1; i <= n; i++) { if (d[i] == INF) { System.out.print("∞ "); } else { System.out.print(d[i] + " "); } } System.out.println(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Sentry-X

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

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

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

打赏作者

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

抵扣说明:

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

余额充值