深搜(DFS)和宽搜(BFS)

本文详细介绍了深度优先搜索(DFS)与宽度优先搜索(BFS)的基本概念、特点及应用,通过对比两者的不同之处,辅以典型例题解析,帮助读者深入理解这两种搜索算法。

相同点:

深搜和宽搜都可以对空间进行遍历,搜索的结构都是树

不同点:

深搜(DFS):(暴搜)(直男)(执着的人)

(1)尽可能往深了搜,当搜到叶节点(简称搜到头)就会回溯,然后再搜下一个,然后再回溯,然后再搜下一个,然后再回溯,再搜下一个......(边回去边看能不能往下走是不是真的无路可走了)。

(2)用栈(stack)实现

(3)搜的时候只需要记录这条路径上的所有点,因此使用的空间和高度是成正比的,越深存的越多

(4)不具最短性

(5)回溯,剪枝

(6)每个DFS都对应一条搜索树

(7)算法、思路比较奇怪的、需要空间比较大的都用DFS

(8)最重要的就是顺序,要用什么样的顺序来把情况都遍历一遍

模型:

void dfs(int step)
{
	//判断边界
	{
		...
	}
	
	//尝试每一种可能(每一种尝试就是一种扩展) 
	 for(i = 1 ; i <= n ; i ++)
	{
		
	} 
	
	//返回
	return; 
}

题目:

题目1:全排列

题目描述:

给定一个整数 n,将数字 1∼n排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数 n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

数据范围

1≤n≤7

输入样例:

3

输出样例:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

AC代码:

啊哈版本:

#include<bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

int a[N],book[N],n; //a表示小盒子

void dfs(int step) //step表示现在站在第几个盒子面前
{
	int i;
	if(step == n + 1) // 如果站在第n + 1个盒子面前,则表示前n个盒子
	{
		// 输出一种排列(1~n号盒子中的扑克牌编号)
		for(i = 1 ; i <= n ;i ++)  cout << " " << a[i];
		cout << endl; 
		return;
	} 
	//此时站在第step个盒子面前,应该放哪张牌呢?
	//按照1,2,3,...,n的顺序一一尝试
	for(i = 1 ; i <= n ; i ++)
	{
		//判断扑克牌i是否还在手上
		if(book[i] == 0) // book[i]等于0表示i号扑克牌在手上
		{
			//开始尝试使用扑克牌;
			a[step] = i; // 将i号牌放入第step个盒子中
			book[i] = 1; // 将book[i]设为1,表示i号扑克牌已经不在手上
			
			//第step个盒子已经放好,接下来需要下一个盒子 
			dfs(step + 1);
			book[i] = 0; // 这是非常重要的一步,一定要将刚才尝试的扑克牌收回,才能进行下一次尝试 
		} 
	} 
	return; 
} 

int main()
{
	ios::sync_with_stdio(false);
	cin >> n;
	dfs(1);
	getchar();
	getchar();
	return 0;
}

y总版本 :

#include<bits/stdc++.h>
#define endl '\n'
#define inf 0x3f3f3f3f

using namespace std;

typedef long long ll;

const int N = 10; 
 
int n;
int state[N]; // 0表示还没放,1~n表示放了哪些数 
bool used[N]; // true表示用过,false表示还未用过 

//u表示一个分支(一个方案)的第几位 
void dfs(int u)
{
	if(u > n) // 边界 
	{
		for(int i = 1 ; i <= n ; i ++)  printf("%d ",state[i]);
		puts("");
		
		return;
	}  
	
	//依次枚举每个分支,即当前位置可以填哪些数
	for(int i = 1 ; i <= n ; i ++)
	{
		//没有被用过 
		if(used[i] == 0)
		{
			state[u] = i;
			used[i] = true;
			dfs(u + 1);
			
			//恢复现场
			state[u] = 0;
			used[i] = false; 
		} 
	} 
} 

int main()
{   
	scanf("%d",&n);
	
	dfs(1); // 传入第一个分支第一位 
	
    return 0; 
}

题目2(DFS + 剪枝)

题目描述:

所谓虫食算,就是原先的算式中有一部分被虫子啃掉了,需要我们根据剩下的数字来判定被啃掉的字母。

来看一个简单的例子:

 43#9865#045
+  8468#6633
--------------
 44445506978

其中 # 号代表被虫子啃掉的数字。

根据算式,我们很容易判断:第一行的两个数字分别是 5和 3,第二行的数字是 5。

现在,我们对问题做两个限制:

首先,我们只考虑加法的虫食算。这里的加法是 N进制加法,算式中三个数都有 N 位,允许有前导的 0。

其次,虫子把所有的数都啃光了,我们只知道哪些数字是相同的,我们将相同的数字用相同的字母表示,不同的数字用不同的字母表示。

如果这个算式是 N进制的,我们就取英文字母表的前 N 个大写字母来表示这个算式中的 0 到 N−1 这 N 个不同的数字:但是这 N 个字母并不一定顺序地代表 0 到 N−1。

输入数据保证 N个字母分别至少出现一次。

   BADC
+  CBDA
----------
   DCCC

上面的算式是一个 4进制的算式。很显然,我们只要让 ABCD分别代表 0123,便可以让这个式子成立了。

你的任务是,对于给定的 N进制加法算式,求出 N个不同的字母分别代表的数字,使得该加法算式成立。输入数据保证有且仅有一组解。

输入格式

输入包含 4行。

第一行有一个正整数 N(N≤26),后面的3行每行有一个由大写字母组成的字符串,分别代表两个加数以及和。这3个字符串左右两端都没有空格,并且恰好有 N位。

输出格式

输出包含一行。在这一行中,应当包含唯一的那组解。解是这样表示的:输出 N个数字,分别表示 A,B,C……所代表的数字,相邻的两个数字用一个空格隔开,不能有多余的空格。

输入样例:

5
ABCED
BDACE
EBBAA

输出样例:

1 0 3 4 2

思路:

搜索顺序:依次枚举每个字符对应哪个数字
剪枝:
1.从低位向高位依次考虑每一位:
a,b,c,t
被加数 加数 和 进位
(a+b+t) mod n=c

2.由于和也是n位数 ,因此最高位不可以有进位

3.从最低位开始枚举每个未知数

AC代码:

#include<bits/stdc++.h>

using namespace std;

const int N = 30;

int path[N]; // 每个字母对应的数字
int q[N]; // 从低位到高位字母出现的顺序
int book[N];
char e[3][N];
int n;

bool check() 
{
    int i;
    int t = 0;
    for (i = n - 1; i >= 0 ; i --) 
	{
        int a = path[e[0][i] - 'A'];
		int b = path[e[1][i] - 'A'];
		int c = path[e[2][i] - 'A'];
        if (a != -1 && b != -1 && c != -1)
		{   
            // a b c 的数值都确定了
            if (t == -1) 
			{ 
                if ((a + b) % n != c && (a + b + 1) % n != c)  return false; //     // 进位不确定 当t=0和t=1都不成立的时候返回false
                if (i == 0 && a + b >= n)  return false; // 如果最高位有进位返回false
            }
            else 
			{
                if ((a + b + t) % n != c)  return false; // t确定的时候
                if (i == 0 && a + b + t >= n)  return false;
                t = (a + b + t) / n;// 进位为这个
            }
        }
        // 如果a b c 没用都确定 则 进位也不确定 t=-1
        else  t = -1;
    }
    return true;
}

// 注意 这里dfs定义为bool型 因为要判断每一位是否出错然后剪枝
// 如果有一位不成立 则可以终止这条子树的搜索
bool dfs(int u)
{
    int i;
    if (u == n)  return true;
    for (i = 0; i < n ; i ++) // 枚举数字
    {  
        if (!book[i]) 
        {
           book[i] = true;
           path[q[u]] = i; // 如果这个数字没使用过 赋值给当前最右的字母 q[u]
           if (check() && dfs(u + 1))  return true; // 如果check没问题dfs下一个 有一位出问题就返回false
           else 
           {
               path[q[u]] = -1;
               book[i] = false;
           }
        }
     }
    return false;
}
int main()
{
    int i,j;
    int tail = 0;
    cin >> n;
    for (i = 0 ; i < 3 ; i ++)  cin >> e[i];
    for (i = n - 1, tail = 0 ; i >= 0 ; i --)//从最低位开始枚举三个字符串的对应位
    {
        for (j = 0 ; j < 3 ; j ++)
        {
            int c = e[j][i] - 'A';
            if (!book[c])//如果这个字母没标记 这时的book是记录字母是否出现过
            {
                book[c] = true;
                q[tail ++] = c;//放入队列 记录字母从低到高的出现顺序
            }
        }
     }
    memset(book, 0, sizeof book); //将book数组清0 后面的book记录数字是否使用过
    memset(path, -1, sizeof path);
    dfs(0);
    for (i = 0; i < n ; i++)  cout << path[i] << " ";
    return 0;
}


宽搜(BFS):(海王)(稳重的人)

(1)一层一层搜,可以同时看很多很多条路

(2)用队列(queue)实现

(3)搜的时候会把一层的节点都存下来,那么它所需要的空间就是指数级别的,很大,因此宽搜在空间上吃大亏

(4)因为宽搜是一层一层往外搜,所以它每次搜的点都是最近的,因此它和最短路有关系

题目:

题目1:

小哼解救小哈:

#include<bits/stdc++.h>

using namespace std;

struct note
{
	int x; // 横坐标 
	int y; // 纵坐标
	int f; // 父亲在队列中的编号,本题不要求输出路径,可以不需要f
	int s; 
};
int main()
{
    struct note que[2501]; // 因为地图大小不超过50*50,因此队列扩展不会超过2500个
	
	int a[51][51] = {0},book[51][51] = {0}; // 数组book的用作是记录哪些点已经在队列中了,防止一个点被重复扩展,并全部初始化为0
	
	//定义一个用于表示走的方向的数组
	int next[4][2] = { {0,1}, // 向右走 
		                {1,0}, // 向下走
						{0,-1}, // 向上走
						{-1,0} }; //向上走 
    
    int head,tail;
	int i,j,k,n,m,startx,starty,p,q,tx,ty,flag;
	
	cin >> n >> m;
	for(i = 1 ; i <= n ; i ++)
	{
		for(j = 1 ; j <= m ; j ++)
		{
			cin >> a[i][j];
		}
	}
	
	cin >> startx >> starty >> p >> q;
	
	//队列初始化
	head = 1;
	tail = 1;
	
	//往队列插入迷宫入口坐标
	que[tail].x = startx;
	que[tail].y = starty;
	que[tail].f = 0;
	que[tail].s = 0;
	tail ++;
	book[startx][starty] = 1;
	
	flag = 0; // 用来标记是否到达目标点,0表示暂时还没有到达,1表达到达
	
	//当队列不为空的时候循环
	while(head < tail)
	{
		//枚举4个方向
		for(k = 0 ; k <= 3 ; k ++)
		{
			//计算下一个点的坐标
			tx = que[head].x + next[k][0];
			ty = que[head].y + next[k][1];
			
			//判断是否越界
			if(tx < 1 || tx > n || ty < 1 || ty > m)  continue;
			
			//判断是否是障碍物或者已经在路径中
			if(a[tx][ty] == 0 && book[tx][ty] == 0)
			{
				//把这个点标记为已经走过
				//注意宽搜每个点只入队一次,所以和深搜不同,不需要将book数组还原
				book[tx][ty] = 1;
				
				//插入新的点到队列中
				que[tail].x = tx;
				que[tail].y = ty;
				que[tail].f = head; // 因为这个点是从head扩展出来的,所以它的父亲是head,本题目不需要路径,因此本句可省略
				que[tail].s = que[head].s + 1; // 步数是父亲的步数 + 1
				tail ++; 
			} 
			//如果到目标点了,停止扩展,任务结束,退出循环
			if(tx == p && ty == q)
			{
				flag = 1;
				break; 
			} 
		} 
		if(flag == 1)  break;
		head ++; // 注意这地方千万不要忘记,当一个点扩展结束后,head ++才能对后面的点再进行扩展 
	} 
	
	//打印队列中末尾最后一个点(目标点)的步数
	//注意tail是指向队列队尾(即最后一位)的下一个位置,所以这需要-1;
	cout << que[tail - 1].s;
	
	getchar();
	getchar(); 
	return 0;
} 

题目2:

给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1表示不可通过的墙壁。最初,有一个人位于左上角 (1,1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角 (n,m)处,至少需要移动多少次。

数据保证 (1,1)处和 (n,m) 处的数字为 0,且一定至少存在一条通路。

输入格式

第一行包含两个整数 n和 m。

接下来 n行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。

输出格式

输出一个整数,表示从左上角移动至右下角的最少移动次数。

数据范围

1≤n,m≤100

输入样例:

5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0

输出样例:

8

AC代码:

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

typedef long long ll;

typedef pair<int, int> PII;

const int N = 110;

int n,m;
int g[N][N]; // 存图
int d[N][N]; // 存步数

int bfs()
{
	queue<PII> q;
	
	memset(d, -1 ,sizeof d); // 初始化距离
	d[0][0] = 0; // 表示0走过了,且步数为0
	q.push({0,0}); // 将(0,0)点放入队列
	
	int dx[4] = {-1, 0 , 1, 0}, dy[4] = {0, 1, 0, -1};
	
	while(q.size())
	{
		auto t = q.front(); // 取出队头
		q.pop(); // 弹出对头
		
		for(int i = 0 ; i < 4 ; i ++) // 对当前点枚举四个方向 
		{
			int x = t.first + dx[i], y = t.second + dy[i]; // 下一个点的坐标为(x,y) 
			
			if(x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) // 下一个点的位置合理且不是障碍且没有走过 
			{
				d[x][y] = d[t.first][t.second] + 1; // 移动下一个点步数加1
				q.push({x,y}); // 将(x,y)点放入队列 
			}
		} 
	} 
	return d[n - 1][m - 1]; 
}

int main()
{
    scanf("%d %d",&n,&m);
    
    for(int i = 0 ; i < n ; i ++)
    {
    	for(int j = 0 ; j < m ; j ++)
    	{
    		scanf("%d",&g[i][j]);
		}
	}
	
	int res = bfs();
	printf("%d\n",res);
}

题目3:(抓住那头牛)

题目描述:

农夫知道一头牛的位置,想要抓住它。

农夫和牛都位于数轴上,农夫起始位于点 N,牛位于点 K。

农夫有两种移动方式:

1.从 X 移动到 X−1 或 X+1,每次移动花费一分钟

2.从 X 移动到 2∗X,每次移动花费一分钟

假设牛没有意识到农夫的行动,站在原地不动。

农夫最少要花多少时间才能抓住牛?

输入格式

共一行,包含两个整数N和K。

输出格式

输出一个整数,表示抓到牛所花费的最少时间。

数据范围

0 ≤ N, K ≤ 10^5

输入样例:

5 17

输出样例:

4

AC代码:(数组模拟队列)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<vector>
#include<set>
#include<map>
#define inf 0x3f3f3f3f
#define endl '\n'
#include<utility>
#include<queue>
 
using namespace std;

typedef long long ll;

typedef pair<int, int> PII;

const int N = 2e5 + 10;

int n,k;
int q[N];
int dist[N];

int bfs()
{
	memset(dist, -1, sizeof dist); 
	dist[n] = 0; // n已经走过,0步
	q[0] = n; // 把n加到队列里面去,进行以后的扩展
	
	int hh = 0, tt = 0;
	while(hh <= tt)
	{
		int t = q[hh]; // 取出队头
		hh ++; // 弹出队头元素
		
		if(t == k) return dist[k];
		
		//三种扩展方式
		if(t + 1 < N && dist[t + 1] == -1)
		{
			dist[t + 1] = dist[t] + 1;
			q[++ tt] = t + 1;
		} 
		if(t - 1 >= 0 && dist[t - 1] == -1)
		{
			dist[t - 1] = dist[t] + 1;
			q[++ tt] = t - 1;
		} 
		if(t * 2 < N && dist[t * 2] == -1)
		{
			dist[t * 2] = dist[t] + 1;
			q[++ tt] = t * 2;
		}
	} 
	return -1;
}

int main()
{
	scanf("%d %d",&n,&k);
	
	int res = bfs();
	printf("%d\n",res);
	
	return 0; 
}

AC代码:(STL)

#include<bits/stdc++.h>
#include<queue>

using namespace std;

int const N = 2e5 + 10;

queue<int> q;

int dist[N]; // dist[i]表示到i点一共走过的步数

int bfs(int n, int k) // 参数表示起点和终点
{
	memset(dist , -1 , sizeof dist);
	dist[n] = 0; // n这个点已经走过
	q.push(n); // 把第一个点(n点)加入队列,对以后进行扩展
	
	while(!q.empty())
	{
		int t = q.front(); // t表示当前的点
		q.pop();
		
		// 已经到达终点
		if(t == k)  return dist[t]; 
		
		// 三种扩展方式
		// 右移一步
		if(t + 1 < N && dist[t + 1] == -1) // 当前点的下一个点不过限 且 没有被扩展过
		{
			dist[t + 1] = dist[t] + 1;
			q.push(t + 1);
		}
		
		// 左移一步
		if(t - 1 >= 0 && dist[t - 1] == -1)  // 当前点的下一个点不过限 且 没有被扩展过
		{
			dist[t - 1] = dist[t] + 1;
			q.push(t - 1);
		}
		
		// 
		if(t * 2 < N && dist[t * 2] == -1) // 当前点的下一个点不过限 且 没有被扩展过
		{
			dist[t * 2] = dist[t] + 1;
			q.push(t * 2);
		}
	}
	return -1;
}

int main()
{
	int n,k;
	scanf("%d %d",&n,&k);
	
	printf("%d\n",bfs(n,k));
	
	return 0; 
}

 题目四:最小操作次数(蓝桥模拟赛)

有一个整数 A=2021,每一次,可以将这个数加 1 、减 1 或除以 2,其中除以 2 必须在数是偶数的时候才允许。
例如,2021 经过一次操作可以变成 2020、2022。
再如,2022 经过一次操作可以变成 2021、2023 或 1011。
请问,2021 最少经过多少次操作可以变成 1。 

类似最短路径和最少操作次数这样的题都可以用bfs来求解

答案:14 

分析:

为什么想到用BFS呢?

答:因为bfs就是从一个点出发,在当前位置上可以有上下左右四种走法;而这道题是从2021出发,可以有+1,-1,/2三种走法,所以是同类型的题,我们可以用bfs来求解

AC代码:

import java.io.*;
import java.math.BigInteger;
import java.util.*;
import java.util.stream.Collectors;

public class Main
{
    static PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
    static int N = (int)2e5 + 10;
    static Queue<Integer> q = new LinkedList<>();
    static int cnt[] = new int[N]; // cnt[i]表示n变到i最少需要多少步

    static int R;
    static int C;

    static int bfs(int n)
    {
        Arrays.fill(cnt,-1);
        cnt[n] = 0;
        q.add(n);

        while(!q.isEmpty())
        {
            int t = q.poll();

            if(t == 1)  return cnt[t];

            if(t % 2 != 0 && cnt[t + 1] == -1)
            {
                cnt[t + 1] = cnt[t] + 1;
                q.add(t + 1);
            }

            if(t % 2 != 0 && cnt[t - 1] == -1)
            {
                cnt[t - 1] = cnt[t] + 1;
                q.add(t - 1);
            }

            if(t % 2 == 0  && cnt[t / 2] == -1)
            {
                cnt[t / 2] = cnt[t] + 1;
                q.add(t / 2);
            }
        }
        return -1;
    }

    public static void main(String[] args ) throws IOException
    {
        int n = rd.nextInt();

        pw.println(bfs(n));

        pw.flush();
    }
}

class rd
{
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    static StringTokenizer tokenizer = new StringTokenizer("");

    static String nextLine() throws IOException { return reader.readLine(); }
    static String next() throws IOException
    {
        while(!tokenizer.hasMoreTokens())  tokenizer = new StringTokenizer(reader.readLine());
        return tokenizer.nextToken();
    }
    static int nextInt() throws IOException { return Integer.parseInt(next()); }
    static double nextDouble() throws IOException { return Double.parseDouble(next()); }
    static long nextLong() throws IOException { return Long.parseLong(next()); }
    static BigInteger nextBigInteger() throws IOException
    {
        BigInteger d = new BigInteger(rd.nextLine());
        return d;
    }
}

class math
{
    int gcd(int a,int b)
    {
        if(b == 0)  return a;
        else return gcd(b,a % b);
    }

    int lcm(int a,int b)
    {
        return a * b / gcd(a, b);
    }

    // 求n的所有约数
    List get_factor(int n)
    {
        List<Long> a = new ArrayList<>();
        for(long i = 1; i <= Math.sqrt(n) ; i ++)
        {
            if(n % i == 0)
            {
                a.add(i);
                if(i != n / i)  a.add(n / i);  // // 避免一下的情况:x = 16时,i = 4 ,x / i = 4的情况,这样会加入两种情况  ^-^复杂度能减少多少是多少
            }
        }

        // 相同因子去重,这个方法,完美
        a = a.stream().distinct().collect(Collectors.toList());

        // 对因子排序(升序)
        Collections.sort(a);

        return a;
    }

    // 判断是否是质数
    boolean check_isPrime(int n)
    {
        if(n < 2) return false;
        for(int i = 2 ; i <= n / i; i ++)  if (n % i == 0) return false;
        return true;
    }
}

class PII implements Comparable<PII>
{
    int x,y;
    public PII(int x ,int y)
    {
        this.x = x;
        this.y = y;
    }
    public int compareTo(PII a)
    {
        if(this.x-a.x != 0)
            return this.x-a.x;  //按x升序排序
        else return this.y-a.y;  //如果x相同,按y升序排序
    }
}

class Edge
{
    int a,b,c;
    public Edge(int a ,int b, int c)
    {
        this.a = a;
        this.b = b;
        this.c = c;
    }
}

class Line implements Comparable<Line>
{
    double k; // 斜率
    double b; // 截距

    public Line(double k, double b)
    {
        this.k = k;
        this.b = b;
    }

    @Override
    public int compareTo(Line o)
    {
        if (this.k > o.k) return 1;
        if (this.k == o.k)
        {
            if (this.b > o.b) return 1;
            return -1;
        }
        return -1;
    }
}

 

 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

21RGHLY

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

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

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

打赏作者

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

抵扣说明:

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

余额充值