# 深搜和广搜(dfs和bfs)
**首先声明这只是本人的学习笔记,有些内容并非原创,主要以个人笔记使用,同时分享和我一起开始学习搜索的萌新**
因为时间问题,目前博客没有完全更新,后面会持续更新的。
[TOC]
正式开始之前,先来一些基本代码,也就是所谓的模板😊
```c++
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <stack>
#include <queue>// 队列
using namespace std;
const int lines = 5;
const int cols = 5;
bool MAP[lines][cols] = {
{0, 1, 1, 1, 1},
{0, 0, 1, 0, 1},
{1, 0, 0, 0, 0},
{1, 1, 0, 1, 0},
{1, 1, 1, 1, 0}};
bool vis[lines][cols] = {false};
struct node
{
int x, y;
};
int fx[] = {0, -1, 0, 1};
int fy[] = {-1, 0, 1, 0};
node load[lines * cols];
int cnt = 0;
void BFS(int x, int y)// B - 宽
{
vis[x][y] = true;
queue<node> q;
// queue<node> qq;// 泛型
q.push({x, y});
while (!q.empty())
{
node coor = q.front();
q.pop();
for (int i = 0; i < 4; i++)
{
int newx = coor.x + fx[i], newy = coor.y +fy[i];
if (MAP[newx][newy] == 0 && newx >= 0 && newx < 5 && newy >= 0 && newy < 5 && !vis[newx][newy])
{
node point;
// point.x = newx;
// point.y = newy;
q.push({newx, newy});
// q.push(point);
vis[newx][newy] = true;
// if(MAP[newx][newy] == 2)
// cnt++;
load[cnt].x = newx, load[cnt++].y = newy;//记录走的位置
}
}
}
return;
}
// int fx[] = {0, -1, 0, 1};
// int fy[] = {-1, 0, 1, 0};
void DFS(int x, int y){ // D - 深
for(int i = 0; i < 4; i++){
int newx = x + fx[i], newy = y + fy[i];
if (MAP[newx][newy] == 0 && newx >= 0 && newx < 5 &&
newy >= 0 && newy < 5 && !vis[newx][newy] )
{
vis[newx][newy] = true;
load[cnt].x = newx, load[cnt++].y = newy;
DFS(newx, newy); //回溯
}
}
}
int main()
{
DFS(0, 0);
for (int i = 0; i < cnt; i++)
{
cout << load[i].x << ',' << load[i].y << endl;
}
return 0;
}
//当进行全局搜所时可以用bfs效率会更高;而dfs时间花费更长,会导致超时
//全局最优可以用bfs,比如说迷宫找最短路径
//queue 的基本操作有:
//队,如例:q.push(x); 将x 接到队列的末端。
//出队,如例:q.pop(); 弹出队列的第一个元素,注意,并不会返回被弹出元素的值。
//访问队首元素,如例:q.front(),即最早被压入队列的元素。
//访问队尾元素,如例:q.back(),即最后被压入队列的元素。
//判断队列空,如例:q.empty(),当队列空时,返回true。
//访问队列中的元素个数,如例:q.size()
```
### 一、广度优先搜索(bfs)
##### 用途 全局搜索🔍
迷宫的最短路径
字符串交换达到字符串的最少次数
##### 实现的方式
运用队列进行存储每一个节点,先进先出,当前一个节点的子节点都已经求出后,直接将队列的取出,继续求下一个节点。
#### 步骤
1、首先标注起始位置,同时把起始位置存入队列,同时再开一张布尔类型的地图
```c++
char map[100][100];
bool vis[100][100];//如果需要还可以开一张地图记录这个点是否走过
struct node{
int x,y;//用结构体表示结点
}
```
2、结合题意看需要进行哪几个方向
```c++
//八个方向的时候
int fx[8] = {-1, -1, 0, 1, 1, 1, 0, -1};
int fy[8] = {0, 1, 1, 1, 0, -1, -1, -1};//这里的方向是有顺序的
//四个方向
int fx[4]={-1,0,1,0};
int fy[4]={0,1,0,-1};
```
3、核心,bfs函数
```
void bfs(int x,int y ){
queue<node> q;//定义一个队列,名字为q,q的类型为node
q.push({x,y});
while(!q.empty())
{
}
}
```
一、广度优先搜索(bfs)后面会更新的🤭
### 二、深度优先搜索(dfs)
一般有回溯都是用得dfs!!!
##### 用途
##### 实现方式
#### 运用!!!
1、*迷宫问题*(洛谷p1605)
我的问题主要是不知道什么时候可以计数,就是路数加一,还有就是还要理解一下回溯流程
```c++
//题解
#include<bits/stdc++.h>//ta
using namespace std;
int n,m,t,sx,sy,fx,fy,sum=0;
bool zhang[10][10],use[10][10];
int dx[4]={0,0,1,-1};
int dy[4]={-1,1,0,0};
void dfs (int x,int y)
{
if(x==fx&&y==fy)
{
sum++;
return ;
}
for(int i=0;i<=3;i++)
{
int nx,ny;
nx=x+dx[i];
ny=y+dy[i];
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&zhang[nx][ny]==0&&use[nx][ny]==0)
{
use[nx][ny]=1;
dfs(nx,ny);
use[nx][ny]=0;//回溯
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&t);
scanf("%d%d%d%d",&sx,&sy,&fx,&fy);
for(int i=0;i<t;i++)
{
int tx,ty;
scanf("%d%d",&tx,&ty);
zhang[tx][ty]=1;
}
use[sx][sy]=1;
dfs(sx,sy);
printf("%d",sum);
return 0;
}
```
2、*自然数的拆分问题*(洛谷p2404)
解题思路及基本格式
```c++
#include <iostream>//基本固定格式
using namespace std;
int a[15];//用于装数字的数组
void dfs(int x,int y)//x表示的是第几个盒子,y表示的是当前数字还剩多少
{
if(达到目的)//也是看y的值,y的值就是看输入的数被拆分后还剩多少,若y==0,则达到目的同盒子数也要大于二才行
{
输出解;
return;
}
for(int i=1;i<=方案数;i++)//这里的方案数字是在改变的,而y就是记录有多少方案数的值
{
if(方案可行)//因为需要按照字典序的大小进行排列,所以满足的条件就要有后一位大于前一位
{
保存路径;
dfs(x+1);
回溯;
}
}
}
```
```c++
//具体题解
#include<bits/stdc++.h>
using namespace std;
int a[20]={0};
int n,sum;
void dfs(int x,int y)
{
if(x>2&&y==0)//x不能等于2不然就会输出n本身,重点:因为盒子的个数至少都要大于1,但因为执行dfs时x会加1,所以当只有7的时候,x=2,所以x不可以等于2!!!!
{
for(int i=1;i<=(x-1);i++)
{
printf("%d+",a[i]);
}
printf("%d\n",a[x-1]);
}
for(int i=1;i<=y;i++)
{
if(i>=a[x-1])//后面的数字要大于等于前面的数字
{
a[x]=i;
y-=i;//盒子数也要跟着变
dfs(x+1,y);
a[x]=0;//回溯
y+=i;
}
}
}
int main()
{
scanf("%d",&n);
sum=n;
dfs(1,y);
return 0;
}
```
3、*单词方阵*(洛谷p1101)
①这道题的难点在于匹配一个单词需要一直保持同一个方向,所以需要一个变量来记录这个方向,这个方向弄完后就需要把这个变量回到初始值
②而且需要两张地图
③这里用到了一个memset函数, memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作 。
```
//实际用法
memset(数组名,需要初始化的值,数组的长度)
```
```c++
#include <bits/stdc++.h>
using namespace std;
char a[105][105],ttr[10]="yizhong";//首先把本来的地图读入,然会把目标字符串放上
char ans[105][105];//再来一张地图,记录符合目标字符的位置
int n,d;//d是用于记录方向的 ,n是键盘输入的地图大小
int fx[9]={0,1,1,0,-1,-1,-1, 0, 1};//字符将要走的八个方向
int fy[9]={0,0,1,1, 1, 0,-1,-1,-1};
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++){
cin>>a[i];
}
memset(ans,'*',sizeof(ans));//把第二张地图先全部初始化为*
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(a[i][j]=='y'){//查找y的位置
for(int k=1;k<=8;k++){//沿着y的方向走查找之后的数据
d=k;//记录现在的方向
for(int l=1;l<=6;l++){//一个字母一个字母地对比
int nx=i+fx[d]*l;
int ny=j+fy[d]*l;//坐标沿着同一方向进行下移
if(nx<0||nx>=n&&ny<0||ny>=n||a[nx][ny]!=ttr[l]){
d=0;//消除d对方向的记录
break;//跳出当前方向
}
}
if(d==k){
for(int l=0;l<=6;l++){//从最开始的找到y的点开始替换ans地图中的符号
ans[i+l*fx[d]][j+l*fy[d]]=a[i+l*fx[d]][j+l*fy[d]];
}
}
}
}
}
}
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
printf("%c",ans[i][j]);
}
printf("\n");
}
return 0;
}
```
*4、填涂颜色*(洛谷p1102)
1、难点就是如何确定找到的‘0’是‘1’数字包围圈里面的!!!
2、思路:判断在圈内的数字不容易,那我们可以判断去圈外的,沿着最外层遍历一圈,从最外面的0开始。同时运用到替换,也就是标记,把没有在圈内的0换为3,输出时就只用看数字进行分类输出。
```c++
#include <bits/stdc++.h>
using namespace std;
int a[40][40];
int n;
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};//确定四个方向
void dfs(int x,int y){
for(int i=0;i<4;i++){
int nx=x+dx[i];
int ny=y+dy[i];
if(nx>=1&&nx<=n&&ny>=1&&ny<=n&&a[nx]a[ny]==0){//判断条件为新的节点是否越界
//,新节点是否为‘0’
a[nx][ny]=3;
dfs(nx,ny);//这道题不用回溯!!!
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
scanf("%d",&a[i][j]);
}
}//读入地图
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==1||i==n||j==0||j==n){//首先判断是否为边缘
if(a[i][j]==0){//再次判断是否是0,若是则染色,然后深搜
a[i][j]=3;
dfs(i,j);
}
}
}
}
//最后根据情况输出地图
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(a[i][j]==3){//分情况讨论输出的情况,还要注意输出格式
printf("0 ");
}
if(a[i][j]==0){
printf("2 ");
}
if(a[i][j]==1){
printf("1 ");
}
}
printf("\n");
}
return 0;
}
```
5、*选数*(洛谷p1036)
1、这道题要用深搜
2、这道题还要把判读素数的函数写出来,当然你也可以把它放在主函数里面
3、其实这里的难点是:如何***去重***?(**这道题没有回溯**)
👍答案是:**不降原则****!!!!**
```
举个例子:
比如说在6里面随便选5个数,那么选法都是什么呢?
瞎枚举?
12345
12346
前两个还不会弄混
然后很可能就乱了
少点数可能不会乱
但是多了就不好整了
比如说在100里随便选50个数。
1 2 3 4 5 6 7 8 9 10 11 12......
Die.
所以我们可以运用不降原则:
保证枚举的这些数是升序排列
其实真正的不降原则还可以平
比如 1 2 2 3 3 4......
但是请注意这道题也不能平
否则就有重复数字了
拿6个里面选3个举例子
1 2 3
1 2 4
1 2 5
1 2 6
第一轮枚举完毕。
第二个数加一
1 3 ?
这个“?”应该是4,因为是升序排列
1 3 4
1 3 5
1 3 6
接着,就是这样
1 4 5
1 4 6
1 5 6
第一位是1枚举完毕
第一位是2呢?
2 3 4
2 3 5
2 3 6
2 4 5
2 4 6
2 5 6
就是这样的,枚举还是蛮清晰的吧
以此类推.....
3 4 5
3 4 6
3 5 6
4 5 6
然后就枚举不了了,结束。
所以说,这样就可以避免判重了。
```
我不懂的地方就是不知道如何去重?什么时候用回溯?为什么写成sum+a[i]就可以过但是写成sum=+a[i]不可以通过;还有就是一定要注意数据的范围,很可能会卡数据。
```c++
#include<bits/stdc++.h>
using namespace std;
int n,k;
long long ans=0;
int a[25],sum=0;
bool isprime(int n)
{
for(int i=2;i<=sqrt(n);i++){
if(n%i==0){
return false;
break;
}
}
return true;
}
void dfs(int cnt,int sum,int m){
if(k==cnt){
if(isprime(sum)){
ans++;
}
return;
}
for(int i=m;i<n;i++){//去重的关键!!!
dfs(cnt+1,sum+a[i],i+1);//如果写成sum+=a[i]就是错误的
}
return;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
dfs(0,0,0);
printf("%d\n",ans);
return 0;
}
```
6、*PERKET*(洛谷p2036)
这道题也是用dfs
思路是:看见这种一个数可选可不选,有多种选择方案的时候(即选择的种类数量不清楚时),我们就可以用dfs(其实就是要用到回溯),dfs结束的条件即为数字用完的时候(用一个计数器)
**加一个新知识****:**(可能会用到)
**ios::sync_with_stdio(false);**这个方法还是要解释一下的
在某些题目中,我们使用普通的cin和cout会超时,所以我们每次只能打scanf和printf,然后一堆的占位符巨麻烦),为什么cin和cout比scanf和printf用的时间多? **这是因为C++中,cin和cout要与stdio同步,中间会有一个缓冲,所以导致cin,cout语句输入输出缓慢,这时就可以用这个语句,取消cin,cout与stdio的同步,说白了就是提速,效率基本与scanf和printf一致。**
```c++
#include <bits/stdc++.h>
using namespace std;
int s[15],b[15],minn,ss=1,bb,n;//s用于存储酸度,b用于存储甜度,ss用于表示当前的酸度总和bb用于表示甜度的总和
bool vis[15]; //但是因为甜度是相乘的,所以ss要初始化为1,vis用于表示这个数是否用过
void dfs(int cnt)
{
if(cnt==0)//选完了
{
minn=min(minn,abs(ss-bb));
return ;
}
for(int i=1;i<=n;i++)//利用循环从1到n查找,这里的循环控制的是最开始算酸度和甜度的数字
{
if(vis[i])
{
vis[i]=0;//标记已经用过的数字
ss*=s[i];
bb+=b[i];
cnt--;//这里的而计数指的是主函数外面控制的算总酸度和甜度的数字的个数
dfs(cnt);
vis[i]=1;//回溯
ss/=s[i];
bb-=b[i];
cnt++;
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&s[i],&b[i]);
}
//选择所有材料的时候
for(int i=1;i<=n;i++){
ss*=s[i];
bb+=b[i];
}
minn=abs(ss-bb);
//选择一种的情况
for(int i=1;i<=n;i++){
ss=s[i];
bb=b[i];
minn=min(minn,abs(ss-bb));
}
memset(vis,1,sizeof(vis));
//选择2-n种
for(int i=2;i<n;i++){
ss=1;//要把之前记录的数据清除
bb=0;
dfs(i);
}
printf("%d",minn);
return 0;
}
```
7、*lake counting s*(洛谷p1596)
思路:这道题用dfs和bfs都可以写,但是各有各的优缺点。这里我们先用dfs,后面我们会用bfs做题。其实DFS就是一口气往一个方向搜索,**然后遇到障碍之后再改一个方向搜索**,走过 的地方你可以开一张新的地图vis记录,或者你也可以直接改变改位置的值(一般如果没有对原来的地图要求保存的话,用这种方法会简单些)。
这道题我不懂的地方就是ans变量加的时候位置放在哪!!!
我错的地方是读入的方法不对(**错误的原因就是因为scanf读到换行就会停止,所以不可以用scanf,可以用cin,也可以用getchar**):
我的**错误**读入代码:
```
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
scanf("%c",&map1[i][j]);
}
}
```
题中的**正确**代码:
```
for (int i=0;i<n;i++)
{
for (int j=0;j<m;j++)
cin>>map1[i][j];
}
```
上代码
```c++
//
#include <bits/stdc++.h>
using namespace std;
char map1[105][105];//注意数据的范围,之前提交出现问题就是因为范围开小了
int n,m,ans=0;
int dx[8]={1,-1,0, 0,1,-1,-1, 1};
int dy[8]={0, 0,1,-1,1,-1, 1,-1};
void dfs(int x,int y){
map1[x][y]='.';
for(int i=0;i<8;i++){
int nx=x+dx[i];
int ny=y+dy[i];
if(nx>=0&&nx<n&&ny>=0&&ny<m&&map1[nx][ny]=='W'){
dfs(nx,ny);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=0;i<n;i++)//注意如何读入,字符串里面还有换行符,所以用scanf不行!!!
{
for (int j=0;j<m;j++)
cin>>map1[i][j];
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(map1[i][j]=='W')//在进入dfs之前就要进行判断
{
dfs(i,j);
ans++;
}
}
}
printf("%d",ans);
return 0;
}
```
#### 小结 后面持续更新😊