王道机试C++第九章 搜索 BFS DFS和蓝桥杯真题Day38倒计时23天

第 9 章 搜索

本章介绍程序设计中另一种非常重要的方法——搜索。搜索是一种有目的地枚举问题的解空间中部分或全部情况,进而找到解的方法。然而,与枚举策略相比,搜索通常是有目的的查找,发现解空间的某一子集内不存在解时,它便会放弃对该子集的搜索,而不像枚举那般逐个地检查子集内的解是否为问题的解。

9.1 宽(广)度优先搜索

宽度优先搜索( Breadth First Search, BFS )策略从搜索的起点开始,不断地优先访问当前结点的邻居。也就是说,首先访问起点,然后依次访问起点尚未访问的邻居结点,再按照访问起点邻居结点的先后顺序依次访问它们的邻居,直到找到解或搜遍整个解空间。宽度优先搜索类似于向静止的湖中扔一个石块,波纹以石块为中心依次向外传播。由于宽度优先搜索的这种不断向外扩展的特性,因此常用于搜索最优值的问题。

例 9.1 Catch That Cow

题目大意
夫约翰被告知逃亡奶牛所在的位置,并希望能够立即抓住奶牛。他刚开始站在点 N (0 ≤ N ≤ 100000 )上,并且母牛站在同一条线上的点 K 0 ≤ K ≤ 100000 )上。农夫约翰有两种交通方式:步行和传送。
* 行走:农夫约翰可以在 1 分钟内从任何一点 X 移动到点 X − 1 或点 X + 1
* 传送:农夫约翰可以在 1 分钟内从任何一点 X 移动到 2 X 点。
如果母牛不知道有人要去追它,根本不动,那么农夫约翰需要多长时间才能找回它?
输入:第1行:两个用空间分隔的整数:N和K
输出:第1行:最少的时间,在几分钟内,农民约翰抓住逃亡的牛
心得体会:
这是一个标准得广度优先算法
代码表示:
#include <bits/stdc++.h>
using namespace std;

struct info{
    int pos;
    int time;
};
int main(){
	int n,k;
	scanf("%d%d",&n,&k);
	queue<info>posqueue;
	bool isvisit[100001];
	for(int i=0;i<100001;++i){//判断有没有被访问过 
		isvisit[i]=false;  
	}
	//把起始点加入队列中
	info first;
	first.pos=n;
	first.time=0;
	posqueue.push(first);
	while(posqueue.empty()==false){
		info cur=posqueue.front();
		posqueue.pop();
		if(cur.pos==k){
			printf("%d\n",cur.time);
			break;
		}
		isvisit[cur.pos]=true;//标记一下表示已经加入了 
		//把邻居加入到队列中
		info neighbour ;  
		if(cur.pos-1>=0&&cur.pos-1<=100000&&isvisit[cur.pos-1]==false){
			neighbour.pos=cur.pos-1;  
			neighbour.time=cur.time+1;
			posqueue.push(neighbour); 
		}
		if(cur.pos+1>=0&&cur.pos+1<=100000&&isvisit[cur.pos+1]==false){
			neighbour.pos=cur.pos+1;  
			neighbour.time=cur.time+1;
			posqueue.push(neighbour);
		}
		if(cur.pos*2>=0&&cur.pos*2<=100000&&isvisit[cur.pos*2]==false){
			neighbour.pos=cur.pos*2; 
			neighbour.time=cur.time+1; 
			posqueue.push(neighbour);
		}
	}
    return 0;
}

例 Find The Multiple

题目大意
给定正整数 n ,编写程序找出非零值 n 的倍数 m ,并且 m 的十进制数表示仅包含数字 0 和 1。你可以假设 n 不大于 200 ,且有不超过 100 位的相应 m
输入:输入文件可能包含多个测试用例。每行包含一个值n(1≤n≤200)。包含零的行终止输入。
输出:对于输入值中的每个n值,打印一条包含相应值m的行。m的十进制表示不能包含超过100位数字。如果给定值n有多个解,其中任何一个都是可以接受的
思路提示

使用广度优先,先尝试小的数字再尝试大的。

代码表示
#include <bits/stdc++.h>
using namespace std;

void BFS(int n) {
    queue<long long> myQueue;
    myQueue.push(1); // 压入初始状态
    while (!myQueue.empty()) {
        long long current = myQueue.front();//取出队首 
        myQueue.pop();//弹出队首 
        if (current % n == 0) { //查找成功
            printf("%lld\n", current);
            break;
        }
        myQueue.push(current * 10);
        myQueue.push(current * 10 + 1);//把邻居加入队列中
    }
}
int main() {
    int n;
//下面这个可以写成while(true){ 
    while (scanf("%d", &n) != EOF) {
        if (n == 0) {
            break;
        }
        BFS(n);
    }
    return 0;
}
心得体会

这段代码体现的是广度优先遍历(BFS)算法。在函数 BFS 中,使用了一个队列 myQueue 来实现广度优先搜索。

广度优先遍历是一种遍历或搜索图或树的算法,它从起始节点开始,逐层地向外扩展,先访问当前层的所有节点,然后再访问下一层的节点。在代码中,初始状态为1,将其压入队列。然后,每次从队列中取出队首元素,检查是否满足条件(current % n == 0),如果满足,则输出结果并结束遍历。如果不满足条件,则将当前元素的倍数(current * 10 和 current * 10 + 1)压入队列,以便在下一层进行遍历。

由于广度优先搜索的特性,它会先遍历当前层的所有节点,然后再进入下一层。这是通过队列的先进先出(FIFO)特性来实现的。


9.2 深度优先搜索

last: 宽度优先搜索过程中,获得到一个状态后,立即扩展这个状态,并且保证早得到的状态优先得到扩展。因此,使用队列的先进先出特性来实现先得到的状态先扩展这一特性。

today: 如果在搜索过程中,首先访问起点,之后访问起点的一个邻居,先不访问除该点之外的其他起点的邻居结点,而是访问该点的邻居结点,如此往复,直到找到解,或者当前访问结点已经没有尚未访问过的邻居结点为止,之后回溯到上一个结点并访问它的另一个邻居结点。这样的搜索策略便是深度优先搜索。
类似于人在迷宫中找出口:每遇到一个路口,先往一个既定的方向走到底,直到发现出口或遇到死胡同。发现死胡同后,就回到上一个路口,并选择另外一个方向继续寻找出口

例 A Knight’s Journey

题目表述
背景
骑士每天看着相同的黑白方块感到越来越无聊并决定去世界各地旅行。骑士按照“日”字规则行走。骑士的世界就是他生活的棋盘。骑士生活在比普通 8× 8 棋盘更小的棋盘上,但棋盘的形状仍然是长方形的。你能帮助这位冒险骑士制订旅行计划吗?
问题
找一条能够让骑士遍历棋盘上所有点的路径。骑士可以在任何一块方块上开始或结束他的旅行。
输入:输入以第一行中的一个正整数n开始。以下各行包含n个测试用例。每个测试用例由一行包含两个正整数p和q的线组成,这样就是1≤p×q≤26。这代表一个p×q棋盘,其中p描述有多少不同的平方数字1,…,p存在,q描述有多少不同的方阵存在。这是拉丁字母表中的第一个q字母:A,……
输出:每个场景的输出都以包含“场景#i:”的一行开头,其中i是从1开始的场景的数量。然后打印一行,其中包含字典学上的第一个路径,访问棋盘的所有方格,然后是一条空线。通过连接所访问的方块的名称,应该在一行上给出路径。每个正方形的名称由一个大写字母后跟一个数字组成
对于#3的理解
思路提示
1、马走的方向有八个方向出发。

2、检查扩展后的节点 (nx, ny) 是否满足以下条件:

  • x 坐标小于 0 或大于等于 p(棋盘的行数)
  • y 坐标小于 0 或大于等于 q(棋盘的列数)
  • (nx, ny) 已经被访问过(在 visit 数组中对应位置为 true

如果上述任何一个条件满足,表示扩展后的节点是无效的或者已经被访问过,那么继续下一个循环迭代,不处理该节点。

3、direction[i][0] 表示 x 方向的偏移量,而 direction[i][1] 表示 y 方向的偏移量。这些偏移量代表了骑士在棋盘上移动的八个方向之一(上、下、左、右以及四个对角线方向)。

通过将偏移量 direction[i] 添加到当前节点的坐标 (x, y),我们得到了扩展后节点的坐标 (nx, ny)。这样,(nx, ny) 就成为了下一个可能的节点,我们将在接下来的搜索中对其进行处理。

4、使用了两个字符变量 col 和 row

对于 col,我们将 ny 加上字符 'A',以将其转换为对应的列号。由于 'A' 的 ASCII 值为 65,因此当 ny 为 0 时,ny + 'A' 的结果为 'A',表示第一列;当 ny 为 1 时,ny + 'A' 的结果为 'B',表示第二列,以此类推。

对于 row,我们将 nx 加上字符 '1',以将其转换为对应的行号。由于 '1' 的 ASCII 值为 49,因此当 nx 为 0 时,nx + '1' 的结果为 '1',表示第一行;当 nx 为 1 时,nx + '1' 的结果为 '2',表示第二行,以此类推。

通过这样的转换,我们将扩展后的节点的坐标 (nx, ny) 转换为了对应的列号 col 和行号 row,以便在后续的操作中使用。这些列号和行号可以用于标识和表示节点在棋盘上的位置。

代码表示
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 30;
int p, q; // 棋盘参数
bool visit[MAXN][MAXN]; //标记矩阵判断某节点有没有被访问过 
int direction[8][2] = {
    {-1, -2}, {1, -2}, {-2, -1}, {2, -1}, {-2, 1}, {2, 1}, {-1, 2}, {1, 2}
};

//实现深度优先算法 接受当前节点(x, y)、当前步数 step、已经走过的路径 ans 
bool DFS(int x, int y, int step, string ans) {
    if (step == p * q) { // 这个已经走好到终点搜索成功
        cout << ans << endl << endl;
        return true;
    } else {//试探邻居
//通过使用偏移量来计算扩展后的节点坐标 (nx, ny) 
        for (int i = 0; i < 8; ++i) { // 遍历邻居结点
		    // 扩展状态坐标
            int nx = x + direction[i][0]; //x方向的偏移量
            int ny = y + direction[i][1];//y方向的偏移量
            
           // col 和 row 是用于表示当前节点编号
            char col = ny + 'A'; //ny + 'A' 将 y 坐标转换为对应的字母编号
            char row = nx + '1';// nx + '1' 将 x 坐标转换为对应的数字编号
            if (nx < 0 || nx >= p || ny < 0 || ny >= q || visit[nx][ny]) {
                continue;
            }
//上条件都不满足,表示扩展后的节点是有效的且未被访问过将该节点标记为已访问
            visit[nx][ny] = true; // 标记该点
            //递归调用 DFS 函数继续搜索下一个扩展后节点
            if (DFS(nx, ny, step + 1, ans + col + row)) {
                return true;
            }
            visit[nx][ny] = false; // 取消标记
        }
    }
    return false;
}

int main() {
    int n;
    cin >> n;
    int caseNumber = 0;
    while (n--) {
        cin >> p >> q;
        memset(visit, false, sizeof(visit));
        cout << "Scenario #" << ++caseNumber << ":" << endl;
        visit[0][0] = true; // 标记 A1 点
        if (!DFS(0, 0, 1, "A1")) {
            cout << "impossible" << endl << endl;
        }
    }
    return 0;
}
心得体会

1、根据给定的起始节点 (x, y),开始深度优先搜索。

2、在每一步搜索中,对当前节点进行以下操作:

1)根据预定义的方向数组 direction,计算扩展后的节点的坐标 (nx, ny)

2)将坐标转换为对应的列号和行号,分别存储在 col 和 row 中。

3)检查扩展后的节点 (nx, ny) 是否满足以下条件:

(nx, ny) 已经被访问过(在 visit 数组中对应位置为 true)。

②y 坐标小于 0 或大于等于 q(棋盘的列数)。

③ x 坐标小于 0 或大于等于 p(棋盘的行数)。

④ 如果任何条件满足,则跳过当前节点,继续下一次循环迭代。

⑤ 否则,将 (nx, ny) 标记为已访问,并进行递归调用 DFS 函数:

a、如果递归调用返回 false,表示在该节点的搜索路径下没有找到满足条件的路径,将该节点的标记取消,并继续尝试其他邻居节点。

b、如果递归调用返回 true,表示找到了满足条件的路径,直接返回 true

c、将扩展后节点的坐标 (nx, ny)、步数 step + 1 和路径 ans + col + row 作为参数传递。

3、如果在所有的邻居节点中都没有找到满足条件的路径,那么返回 false,表示无法找到完整路径。

整体思路:通过深度优先搜索算法,在棋盘上搜索从起始节点开始的路径。在搜索过程中,通过递归调用不断扩展当前节点,并检查扩展后的节点是否满足条件。如果找到了满足条件的路径,立即返回 true,否则继续搜索其他可能的路径。如果所有的路径都被搜索完毕,仍然没有找到满足条件的路径,返回 false


广度和深度的区别:

广度优先搜索(BFS)和深度优先搜索(DFS)是两种常见的图搜索算法,它们在搜索顺序、空间复杂度和搜索结果等方面有一些区别。

1、搜索顺序:

BFS:从起始节点开始,逐层地扩展搜索,先访问离起始节点最近的节点,再逐渐向外扩展到离起始节点更远的节点。

DFS:从起始节点开始,沿着路径一直向下搜索,直到达到最深的节点,然后回溯到上一个节点,继续搜索其他路径。

2、空间复杂度:

BFS:需要使用队列来存储待访问的节点,以及标记已访问的节点。在最坏情况下,当图为树状结构时,BFS 的空间复杂度为 O(V),其中 V 是图中节点的数量。

DFS:通常使用递归栈来存储函数调用的信息。在最坏情况下,当图为链状结构时,DFS 的空间复杂度为 O(V),其中 V 是图中节点的数量。然而,在实际应用中,DFS 的空间复杂度往往较低,因为它只需要存储一条路径上的节点。

3、搜索结果:

BFS:保证找到的路径是离起始节点最近的路径,因此在无权图,BFS 通常用于寻找最短路径。

DFS:不能保证找到的路径是最短路径,因为它会优先探索深度较大的路径。

4、应用场景:

BFS:适用于需要找到最短路径或层次遍历的问题,如迷宫最短路径、社交网络中的关系查找等。DFS:适用于需要遍历整个图或搜索特定路径的问题,如图的连通性判断、拓扑排序、深度优先遍历等。

具体实例理解:

迷宫问题:
假设你被困在一个迷宫中,你需要找到一条从起点到终点的路径。在这种情况下,BFS 和 DFS 的不同表现如下:

BFS:你会选择先尝试从起点开始,逐层地向外扩展搜索。你先探索离起点最近的位置,并在每一层继续探索与当前位置相邻的位置,直到找到终点或者遍历完整个迷宫。BFS 可以保证你找到的路径是最短路径。

DFS:你会选择从起点开始,沿着一条路径一直深入迷宫,直到遇到死胡同或者找到终点。如果遇到死胡同,你会回溯到上一个节点,继续搜索其他路径。DFS 不一定能找到最短路径,但它可以帮助你遍历整个迷宫,找到其中的一条路径。

社交网络中的关系查找:
假设你在一个社交网络中,想要查找与你有共同朋友的人。在这种情况下,BFS 和 DFS 的不同表现如下:

BFS:你会选择从自己开始,首先查找与你直接相连的人(一度关系),然后查找与这些人直接相连的人(二度关系),依次类推。BFS 可以帮助你逐层地扩展查找,找到与你有共同朋友的人,并且先找到的人与你的关系更近。

DFS:你会选择从自己开始,深入其中一位朋友的朋友(二度关系),然后继续深入这个人的朋友(三度关系),直到找到与你有共同朋友的人或者遍历完整个网络。DFS 可以帮助你搜索到可能较远的关系,但无法保证先找到的人与你的关系更近。


蓝桥杯真题

[蓝桥杯 2021 国 BC] 大写

题目描述

给定一个只包含大写字母和小写字母的字符串,请将其中所有的小写字母转换成大写字母后将字符串输出。

输入格式:输入一行包含一个字符串。

输出格式:输出转换成大写后的字符串。

代码表示
#include <bits/stdc++.h>
using namespace std;

int main() {
    string arr;
    cin >> arr;
    for (int i = 0; i < arr.size(); ++i) {
        if (arr[i] >= 'a' && arr[i] <= 'z') {
            cout << char(arr[i] - 'a' + 'A');
        } else {
            cout << arr[i];
        }
    }
    return 0;
}
心得体会

1、注意用到字符串里面字符串的大小使用arr.size()不要忘记加括号

2、arr[i] - 'a' 时,我们实际上是计算 arr[i] 相对于小写字母 a 的偏移量。假设 arr[i] 对应小写字母 b,那么 arr[i] - 'a' 的结果就是 1,因为小写字母 b 在小写字母表中的位置相对于 a 往后偏移了一个位置。再加上 'A' 的ASCII码值,就可以将偏移量转换为大写字母的ASCII码值。


[蓝桥杯 2020 省 B1] 整除序列

题目描述

有一个序列,序列的第一个数是 n,后面的每个数是前一个数整除 2,请输出这个序列中值为正数的项。

输入格式 输入一行包含一个整数 n。

输出格式 输出一行,包含多个整数,相邻的整数之间用一个空格分隔,表示答案。

代码表示
#include <bits/stdc++.h>
using namespace std;

int main() {
    long long n;
    cin>>n;
    while(n){
    	cout<<n<<" ";
		n=n/2; 
	}
    return 0;
}
心得体会

1、注意要使用long long类型,有一天我也会因为这个出错!!

2、注意代码得逻辑就是要先输出再进行算数。


[蓝桥杯 2020 省 AB3] 日期识别

题目描述

小蓝要处理非常多的数据, 其中有一些数据是日期。在小蓝处理的日期中有两种常用的形式:英文形式和数字形式。英文形式采用每个月的英文的前三个字母作为月份标识,后面跟两位数字表示日期,月份标识第一个字母大写,后两个字母小写, 日期小于 10 时要补前导0。1月到 12 月英文的前三个字母分别是 JanFebMarAprMayJunJulAugSepOctNovDec

数字形式直接用两个整数表达,中间用一个空格分隔,两个整数都不写前导 0。其中月份用 1 至 12 分别表示 1 月到 12 月。输入一个日期的英文形式, 请输出它的数字形式。

输入格式 输入一个日期的英文形式。

输出格式 输出一行包含两个整数,分别表示日期的月和日。

代码表示
#include <bits/stdc++.h>
using namespace std;

string a[12]={"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
int main()
{
    int m=0,d=0;
    //定义了两个字符串变量st和month
    string st,month="";
    cin>>st;//输入日期字符串
    month=st.substr(0,3);//提取月份的英文缩写
    for(int i=0;i<12;i++){
		if(month==a[i]){
		//数组下标(0~11)转换为实际的月份(1~12)
			m=i+1;
			break;
		}
    }
    d=int(st[3]-'0')*10+int(st[4]-'0');//第四位和第五位字符转换为对应的数字 
    cout<<m<<' '<<d;
}
心得体会

1、st.substr(0,3)表示从字符串st的起始位置开始(即索引0),提取长度为3的子字符串。C++标准库中的string类提供的成员函数substr,用于从字符串中提取子字符串。

2、st[3]st[4]st是一个字符串变量,通过下标索引可以访问字符串中的特定字符st[3]表示访问字符串st中索引为3的字符,即日期的十位数字;st[4]表示访问索引为4的字符,即日期的个位数字。

int(st[3]-48)int(st[4]-48):ASCII码中数字0到9的顺序是连续的,'0'对应的ASCII码是48,因此通过将字符减去48后得到的就是对应的数字值。这里将st[3]st[4]转换为对应的数字。

3、d=int(st[3]-'0')*10+int(st[4]-'0');在这段代码中我们可以把 ‘0’ 写成 48同样也可以。由于48就是0的ASCII码值大小。

### 关于BFSDFS算法的蓝桥杯竞赛题目 #### 广度优先搜索BFS) 广度优先搜索是一种用于遍历或搜索树或图结构的算法。该方法从根节点开始,先访问离根最近的所有邻居节点,然后再进入下一层继续这一过程直到遍历结束[^2]。 #### 深度优先搜索DFS深度优先搜索也是一种用于遍历或搜索树或图的方法。不同于广度优先搜索的是,这种方法会尽可能深地沿着每个分支前进,只有当遇到已经访问过的顶点或是死胡同时才会回溯并尝其他未探索的路径[^1]。 #### 示例代码实现 下面给出一段Python代码来展示如何利用这两种算法解决实际问题: ##### 使用DFS求解全排列问题 ```python N = 10 path = [0] * N state = [False] * N def dfs(u): if u == n: for i in range(n): print(path[i], end=' ') print() return for i in range(n): if not state[i]: path[u] = i + 1 state[i] = True dfs(u + 1) # 处理下一个位置 # 恢复现场 path[u] = 0 state[i] = False n = int(input()) dfs(0) # 从第0个位置开始处理 ``` 这段程序实现了基于DFS的全排列生成器,可以用来作为练习题目的基础框架之一[^3]。 ##### 解决迷宫问题的例子 (假设使用队列实现BFS) ```python from collections import deque maze = [ "########", "#S.....#", "#.######", "#..#.##.", "#...G.#.", "########" ] start, goal = None, None for y in range(len(maze)): for x in range(len(maze[y])): if maze[y][x] == 'S': start = (y,x) elif maze[y][x] == 'G': goal = (y,x) def bfs(): queue = deque([start]) visited = set(start) while queue: pos = queue.popleft() if pos == goal: return True for dy,dx in ((0,-1),(0,+1),(-1,0),(+1,0)): next_pos = (pos[0]+dy,pos[1]+dx) if all([ 0<=next_pos[0]<len(maze), 0<=next_pos[1]<len(maze[next_pos[0]]), maze[next_pos[0]][next_pos[1]] != '#', next_pos not in visited, ]): queue.append(next_pos) visited.add(next_pos) return False print(bfs()) ``` 此段代码展示了怎样运用BFS去查找迷宫中的出路,其中起点标记为'S'而终点则被记作'G'^[2].
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值