C++ 双向广度搜索,嚯嚯!不就是双指针理念吗

news2024/11/28 3:34:51

1. 前言

在线性数据结构中搜索时,常使用线性搜索算法,但其性能偏低下,其性能改善方案常有二分搜索和双指针或多指针搜索算法。在复杂的数据结构如树和图中,常规搜索算法是深度和广度搜索。在深度搜索算法过程中常借助剪枝或记忆化方案提升搜索性能。广度搜索算法过程中常见的性能优化方案为双向广度搜索和启发式搜索。双向广度搜索可以认为是图论中的双指针搜索方案,本文将和大家深入探讨其算法细节。

图中常见的操作为最短路径查找。如在下面的无向无权重图中查找节点1到节点6之间的最短路径,可以直接使用广度搜索算法找到。

1.png

无向无权重图中,直接使用广度搜索算法查找节点之间的最短路径的基本模板代码:

#include <iostream>
#include <queue>
using namespace std;
//邻接矩阵
int graph[100][100];
//记录是否被访问过
int vis[100];
//边数与顶点数
int n ,m;
//节点距离起始点的最短路径
int dis[100];
void init() {
	for(int i=1; i<=n; i++) {
		for(int j=1; j<=n; j++) {
			graph[i][j]=0;
		}
		dis[i]=0;
		vis[i]=0;
	}
}
void addEdge() {
	int f,t;
	for(int i=1; i<=m; i++) {
		cin>>f>>t;
		graph[f][t]=1;
		graph[t][f]=1;
	}
}

//广度搜索
void bfs(int start,int end) {
	//队列
	queue<int> myq;
	//初始化队列
	myq.push(start);
	vis[start]=1;
	dis[start]=0;
	while( !myq.empty() ) {
		int size=myq.size();
		//找到队列中的所有节点
		for(int i=0; i<size; i++) {
			int t= myq.front();
			myq.pop();
			//搜索到终点 
			if(t==end)return;
			//扩展所有子节点入队列
			for(int j=1; j<=n; j++) {
				if(graph[t][j]==1 &&  vis[j]==0) {
					myq.push(j);
					vis[j]=1;
					dis[j]=dis[t]+1;
				}
			}
		}
	}
}
void show() {
	for(int i=1; i<=n; i++)
		cout<<i<<"-"<<dis[i]<<"\t";
}
int main(int argc, char** argv) {
	cin>>n>>m;
	init();
	addEdge();
	bfs(1,n);
	show();
	return 0;
}

测试结果:

2.png

当图中的节点很多,关系较复杂时,直接使用广度搜索其时间复杂度非常大。对于上述问题,既然已经知道了起点和终点,可以使用类似于双指针的方案,让搜索分别从起点和终点开始,从两端相向进行。这样可以减少一半的搜索量,此种搜索方案称为双向广度搜索。

2. 初识双向广度搜索

不是任何时候都可以使用双向广度搜索,只有当起点和终点已知情况方可使用。如下图所示,从起点向终点方向的搜索称为正向搜索,从终点向起点方向的搜索称为逆向搜索。

3.png

下面演示双向搜索的过程。

  • 双向广度搜索实现过程中,可以使用2个队列,也可以仅使用1个队列。这里先使用 2个队列的方案。正向搜索方向的队列命名为q1,逆向搜索方向的队列命名为q2

4.png

  • 初始化2个队列。q1中压入起点,q2中压入终点。

5.png

  • 扩展方案。如果q1的尺寸小于q2,则扩展q1队列,否则,扩展q2队列。初始q1q2两个队列的尺寸相同,此时可以选择扩展q1队列。扩展后的结果如下图所示。

6.png

  • 下面扩展q2 队列。

7.png

  • 至此,继续扩展q1队列。发现节点3和节点2的子节点4、5已经被访问过,且存放在q2中,可以此判断双向搜索相遇,可以认定双向搜索结束。
    15.png

对于相遇条件总结一下。

当对一个队列中的节点进行扩展时,发现此节点的子节点已经被另一个搜索队列扩展,可以认定两个搜索过程相遇。类似于两个施工队相向方向挖一条壕沟时,两者一定是相遇到对方挖通的位置,也就是说当一方挖到了对方正在挖的位置。

也可以在搜索结束后,查看那一个队列不为空,不空的队列中的节点即为相遇的节点。

如面使用代码描述上述的整个流程。

#include <iostream>
#include <queue>
using namespace std;
//邻接矩阵
int graph[100][100];
//边数与顶点数
int n ,m;
//正向搜索时,节点距离起始点的最短路径,也可以记录节点是否被访问过
int dis[100];
//逆向搜索时,节点距离终点的最短路径
int dis_[100];
//初如化
void init() {
	for(int i=1; i<=n; i++) {
		for(int j=1; j<=n; j++) {
			graph[i][j]=0;
		}
        //因为节点和自己的距离为 0,用-1 表示没有被访问,
		dis[i]=-1;
		dis_[i]=-1;
	}
}
//构建图
void addEdge() {
	int f,t;
	for(int i=1; i<=m; i++) {
		cin>>f>>t;
		graph[f][t]=1;
		graph[t][f]=1;
	}
}

//广度搜索
int bfs(int start,int end) {
	//正向队列
	queue<int> q1;
	//逆向队列
	queue<int> q2;
	//初始化队列
	q1.push(start);
	dis[start]=0;
	q2.push(end);
	dis_[end]=0;
     //任意队列为空时结束
	while( !q1.empty() && !q2.empty() ) {
         //计算队列的尺寸
		int s1=q1.size();
		int s2=q2.size();
		if(s1<=s2) {
			//扩展 q1
			for(int i=0; i<s1; i++) {
				int t= q1.front();
				q1.pop();
				//扩展所有子节点入队列
				for(int j=1; j<=n; j++) {
                      //不是子节点或者被访问过都跳过
					if(graph[t][j]==0 ||  dis[j]!=-1)continue;
                      //入队
					q1.push(j);
                      //计算距离
					dis[j]=dis[t]+1;
                      //如果出现在对方队列中,搜索结束
					if( dis_[j]!=-1)return dis[t]+dis_[j]+1;
				}
			}
		} else {
			//扩展 q2
			for(int i=0; i<s2; i++) {
				int t= q2.front();
				q2.pop();
				//扩展所有子节点入队列
				for(int j=1; j<=n; j++) {
					if(graph[t][j]==0 ||  dis_[j]!=-1)continue;
					q2.push(j);
					dis_[j]=dis_[t]+1;
					if(  dis[j]!=-1)return dis_[t]+dis[j]+1;
				}
			}
		}
	}
}
int main(int argc, char** argv) {
	cin>>n>>m;
	init();
	addEdge();
	int d= bfs(1,n);
	cout<<d;
	return 0;
}

也可以使用一个队列实现双向搜索算法。下面演示使用一个队列实现双向搜索流程。为了区分节点是属于正向还逆向搜索到的节点,用两种颜色分别表示,红色表示正向搜索到的节点,绿色表示逆向搜索到的节点。

8.png

  • 初始化队列。把起点和终点分别压入队列中。

9.png

  • 按正常流程对队列中的节点进行扩展。如下,扩展节点6的子节点4、5入队列。

10.png

  • 继续扩展节点1的子节点2、3入队列。

11.png

  • 继续扩展时,发现需要扩展的子节点已经存在于队列中,说明,已经相遇了。

12.png

3. 深度理解

下面通过几个案例让大家更深入的理解双向广度搜索。

3.1 字串变换,

题目来自于https://www.luogu.com.cn/problem/P1032

题目描述

已知有两个字串 A、B 及一组字串变换的规则(至多 6 个规则),形如:

  • A1->B1
  • A2->B2

规则的含义为:在 A 中的子串 A1 可以变换为 B1A2 可以变换为 B2……。

例如:A=abcdB=xyz

变换规则为:

  • abc->xu,ud->y,y->yz

则此时,A 可以经过一系列的变换变为 B,其变换的过程为:

  • abcd->xud->xy->xyz

共进行了 3 次变换,使得 A 变换为 B

输入格式

第一行有两个字符串 A,B

接下来若干行,每行有两个字符串 Ai,Bi,表示一条变换规则。

输出格式

若在 10 步(包含 10 步)以内能将 A 变换为 B,则输出最少的变换步数;否则输出 NO ANSWER!

样例 #1

样例输入 #1

abcd xyz
abc xu
ud y
y yz

样例输出 #1

3

算法分析:

根据样例绘制转换流程图。

13.png

虽然根据样例绘制出来是线性数据结构,但因规则可以很多,如果再添加如下几条新规则,则转换流程图就可能是图结构。

  • cd->z
  • ab->xy

14.png

以最大可能性考虑此题,其转换过程就是一个无向无权重图结构,且本质就是在图中查找起点到终点的最短路径。可以直接使用BFS算法,当数据量较大时,可以使用双向BFS搜索算法。下面代码使用双向广度搜索方案。

#include <iostream>
#include <queue>
#include <map>
using namespace std;
//开始和结束字符串
string s,e;
//存储转换规则
map<string,string> rule;
//存储节点至起点的距离
map<string,int> sdis;
//存储节点至能终点的距离
map<string,int>  edis;
//记录规则数量
int size=0;

int extend(queue<string> &q,map<string,int> &dis,map<string,int> &dis_,int type) {
	//队列的大小
	int size=q.size();
	//遍历队列
	for(int i=0; i<size; i++) {
		string t=q.front();
		string t1=t;
		q.pop();
		//查找可以转换的状态
		map<string,string>::iterator begin=rule.begin();
		map<string,string>::iterator end=rule.end();
		while(begin!=end) {
			string first= begin->first,second=begin->second;
			//如果是逆向搜索,规则也要改成逆向 
			if(type)first=begin->second,second=begin->first;
			//查找是否有可以转换的子串
			int pos= t1.find(  first );
			if(pos!=-1) {
				//得到可转换字符串
				t1.replace(pos,first.size(),second);
				//已经访问过
				if( dis[t1] ) continue;
				//没有访问过
				q.push(t1);
				dis[t1]=dis[t]+1;
				if(dis_[t1] )
					return dis_[t1]+dis[t]-1;
				t1=t;
			}
			begin++;
		}
	}
	return 0;
}

int bfs() {
	//正向队列
	queue<string> zxq;
	//反向队列
	queue<string> fxq;
	//初始化队列
	zxq.push(s);
	fxq.push(e);
	//即表示距离也表示访问过 
	sdis[s]=1; 
	edis[e]=1;
	int res=0;

	while( !zxq.empty() &&  !fxq.empty() ) {
		int size1=zxq.size();
		int size2=fxq.size();
		if( size1<=size2 ) res= extend(zxq,sdis,edis,0); //扩展正向队列
		else res=  extend(fxq,edis,sdis,1); //扩展反向队列
		if(res!=0)break;
	}

	return res;
}


int main() {
	cin>>s>>e;
	string f,t;
	while(cin>>f>>t) {
		rule[f]=t;
		size++;
	}
	int res=bfs();
	cout<<res;
	return 0;
}

3.2 八数码问题

题目描述:

八数码问题是典型的状态图搜索问题。在一个3×3的棋盘上放置编号为1~88个方块,每个占一格,另外还有一个空格。与空格相邻的数字方块可以移动到空格里。

任务1:指定初始棋局和目标棋局,计算出最少的移动步数;

任务2:输出数码的移动序列。

此题可以使用双向广度搜索算法查找到结果。因为正向和逆向搜索的扩展数量是相同的,可以使用一个队列实现,且正向搜索过的节点状态用1表示,逆向搜索过的节点状态用2表示。当节点和子节点的状态值之和为 3的时表示当正向和逆向搜索相遇。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int ed=123804765,st,f[10][5]={{0,1},{1,0},{-1,0},{0,-1}},s[5][5];
map<int,int> vis,d;
int bfs(int a,int b)
{
    queue<int> q;
    //正向搜索状态设置为 1
    vis[a]=1,d[a]=0;
    //逆向搜索状态设置为 2
    vis[b]=2,d[b]=0;
    //入队列
    q.push(a);
    q.push(b);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        int v=u,x,y;
        //将数放入二维数组
        for(int i=3;i>=1;i--)
            for(int j=3;j>=1;j--)
            {
                s[i][j]=v%10;//分解数字
                v/=10;
                if(!s[i][j]) x=i,y=j;//找出0的位置
            }
        //将0进行移动
        for(int i=0;i<4;i++)
        {
            //向四周搜索
            int sx=x+f[i][0],sy=y+f[i][1];
            //越界检查
            if(sx<1||sx>3||sy<1||sy>3) continue;
            //得到新状态
            swap(s[x][y],s[sx][sy]);
            v=0;//还原成数字状态
            for(int i=1;i<=3;i++)
                for(int j=1;j<=3;j++)
                    v=v*10+s[i][j];
            if(vis[v]==vis[u])
            {
                //如果已经访问过
                swap(s[x][y],s[sx][sy]);
                continue;
            } 
            //如果相遇
            if(vis[v]+vis[u]==3) return d[u]+1+d[v];//起点为1,终点为2,相加为3
            d[v]=d[u]+1;
            vis[v]=vis[u];
            q.push(v);
            swap(s[x][y],s[sx][sy]);
        }
    }
    return -1;
}
int main ()
{
    cin>>st;
    if(st==ed) cout<<0<<"\n";
    else cout<<bfs(st,ed)<<"\n";
    return 0;
}

4. 总结

本文讲解了双向广度搜索算法,和双指针算法一样,让搜索双向同时进行,可以减沙近一半的搜索范围,提升搜索性能。记住,双向搜索算法要求在已知起点和终点的条件方可使用。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

掌握Go并发:Go语言并发编程深度解析

&#x1f3f7;️个人主页&#xff1a;鼠鼠我捏&#xff0c;要死了捏的主页 &#x1f3f7;️系列专栏&#xff1a;Golang全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&…

高程 | 数据的共享与保护(c++)

文章目录 &#x1f4da;标识符的作用域与可见性&#x1f407;作用域&#x1f407;可见性 &#x1f4da;对象的生存期&#x1f407;静态生存期&#x1f407;动态生存期 &#x1f4da;类的静态成员&#x1f407;静态数据成员&#x1f407;静态函数成员 &#x1f4da;类的友元&…

什么是位段?位段的作用是什么?他与结构体有什么关系?

目录 1.什么是位段&#xff1f; 2.位段的内存分配 判断当前机器位段的内存分配形式 1.什么是位段&#xff1f; 位段的声明和结构是类似的&#xff0c;有两个不同&#xff1a; 1.位段的成员必须是 int、unsigned int 或signed int或char 。 2.位段的成员名后边有一个冒号和…

相机图像质量研究(13)常见问题总结:光学结构对成像的影响--鬼影

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

STM32 I2C

目录 I2C通信 软件I2C读写MPU6050 I2C通信外设 硬件I2C读写MPU6050 I2C通信 R/W&#xff1a;0写1读 十轴&#xff1a;3轴加速度&#xff0c;3轴角速度&#xff0c;3轴磁场强度和一个气压强度 软件I2C读写MPU6050 MyI2C.c #include "stm32f10x.h" …

【智能家居入门4】(FreeRTOS、MQTT服务器、MQTT协议、微信小程序)

前面已经发了智能家居入门的1、2、3了&#xff0c;在实际开发中一般都会使用到实时操作系统&#xff0c;这里就以FreeRTOS为例子&#xff0c;使用标准库。记录由裸机转到实时操作系统所遇到的问题以及总体流程。相较于裸机&#xff0c;系统实时性强了很多&#xff0c;小程序下发…

[NSSRound#16 Basic]Web

1.RCE但是没有完全RCE 显示md5强比较&#xff0c;然后md5_3随便传 md5_1M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&md5_2M%C9h%FF%0E%E3%5C%20%95r%D4w…

物流快递管理系统

文章目录 物流快递管理系统一、系统演示二、项目介绍三、13000字论文参考四、系统部分页面展示五、部分代码展示六、底部获取项目源码和万字论文参考&#xff08;9.9&#xffe5;带走&#xff09; 物流快递管理系统 一、系统演示 校园物流快递管理系统 二、项目介绍 主要技术…

用HTML5实现动画

用HTML5实现动画 要在HTML5中实现动画&#xff0c;可以使用以下几种方法&#xff1a;CSS动画、使用<canvas>元素和JavaScript来实现动画、使用JavaScript动画库。重点介绍前两种。 一、CSS动画 CSS3 动画&#xff1a;使用CSS3的动画属性和关键帧&#xff08;keyframes&…

备战蓝桥杯---数据结构之好题分享1

最近几天在刷学校的题单时&#xff0c;发现了几道十分巧妙又有启发性的题&#xff0c;借此来记录分享一下。 看题&#xff1a; 从整体上看似乎没有什么规律&#xff0c;于是我们从小地方入手&#xff0c;下面是图解&#xff1a; 因此&#xff0c;我们用栈的数据结构实现即可&a…

模拟算法总结(Java)

目录 模拟算法概述 练习 练习1&#xff1a;替换所有的问号 练习2&#xff1a;提莫攻击 练习3&#xff1a;Z字形变换 模拟算法概述 模拟&#xff1a;根据题目要求的实现过程进行编程模拟&#xff0c;即题目要求什么就实现什么 解决这类题目&#xff0c;需要&#xff1a; 1…

C 语言 devc++ 使用 winsock 实现 windows UDP 局域网发送消息

U参考来源 U 这里移植到windows 上 &#xff0c;使用 devc 开发。 服务端代码 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <winsock2.h>int main() {WORD sockVersion MAKEWORD(2, 2);WSAD…

【嵌入式移植】6、U-Boot源码分析3—make

U-Boot源码分析3—make all 从【嵌入式移植】4、U-Boot源码分析1—Makefile文章中可知执行make命令的时候&#xff0c;没有指定目标则使用默认目标PHONY&#xff0c;PHONY依赖项为_all all scripts_basic outputmakefile scripts dtbs。 all Makefile中第129行指定默认目标PH…

协调尺度:特征缩放在机器学习中的重要作用

目录 一、介绍 二、背景知识 三、了解功能缩放 四、特征缩放方法 五、特征缩放的重要性 六、实际意义 七、代码 八、结论 一、介绍 特征缩放是机器学习和数据分析预处理阶段的关键步骤&#xff0c;在优化各种算法的性能和效率方面起着至关重要的作用。本文深入探讨了特征缩放的…

蓝桥杯每日一题----单调栈和单调队列

单调栈和单调队列 单调栈 单调栈即栈内的元素是单调递减或者单调递增的&#xff0c;我们通过一个题目来理解。 单调栈模板题 题目描述 给出项数为 n 的整数数列 a 1 … a n a_1…a_n a1​…an​。 定义函数 f ( i ) f(i) f(i)代表数列中第 i 个元素之后第一个大于 a i …

安卓游戏开发框架应用场景以及优劣分析

一、引言 在移动游戏开发领域&#xff0c;选择合适的开发框架是项目成功的关键因素之一。特别是对于安卓平台&#xff0c;由于其开放性和庞大的用户基础&#xff0c;不同的游戏开发框架应运而生&#xff0c;旨在帮助开发者高效地构建游戏应用。以下是一些流行的安卓游戏开发框架…

OpenAI全新发布文生视频模型Sora - 现实,不存在了

OpenAI&#xff0c;发他们的文生视频大模型&#xff0c;Sora了。。。。。 而且&#xff0c;是强到&#xff0c;能震惊我一万年的程度。。。 https://openai.com/sora 如果非要用三个词来总结Sora&#xff0c;那就是“60s超长长度”、“单视频多角度镜头”和“世界模型” &am…

五、DataX源码分析、性能参数优化

DataX源码分析 一、总体流程二、程序入口1.datax.py2.com.alibaba.datax.core.Engine.java3.切分的逻辑并发数的确认 3.调度3.1 确定组数和分组算法3.2 数据传输 三、DataX性能优化1.关键参数2.优化&#xff1a;提升每个 channel 的速度3.优化&#xff1a;提升 DataX Job 内 Ch…

SpringBoot3 + Vue3 由浅入深的交互 基础交互教学

说明&#xff1a;这篇文章是适用于已经学过SpringBoot3和Vue3理论知识&#xff0c;但不会具体如何实操的过程的朋友&#xff0c;那么我将手把手从教大家从后端与前端交互的过程教学。 目录 一、创建一个SpringBoot3项目的和Vue3项目并进行配置 1.1后端配置: 1.1.1applicatio…

php基础学习之作用域和静态变量

作用域 变量&#xff08;常量&#xff09;能够被访问的区域&#xff0c;变量可以在常规代码中定义&#xff0c;也可以在函数内部定义 变量的作用域 在 PHP 中作用域严格来说分为两种&#xff0c;但是 PHP内部还定义一些在严格意义之外的一种&#xff0c;所以总共算三种—— 局部…