五、搜索技术

思考一个迷宫问题:有一个错综复杂的迷宫,让一堆老鼠寻找迷宫的出口,老鼠应该怎么办?

  1. 一只老鼠走迷宫。它在每个路口都选择先走右边(先走左边也可以),能走多远就走多远,直到碰壁无法继续往前走,然后回退一步,这一次走左边,接着继续往下走。用这个办法能走遍所有的路,而且不会重复(这里规定回退不算重复走)。这个思路就是DFS(深度优先搜索)。
  2. 一群老鼠走迷宫。假设老鼠是无限多的,这群老鼠进去后,在每个路口派出部分老鼠探索所有没走过的路。走某条路的老鼠,如果碰壁无法前行,就停下;如果到达的路口已经有其他老鼠探索过了,也停下。很显然,所有的道路都会走到,而且不会重复。这个思路就是BFS(广度优先搜索)。
BFS看起来像“并行计算”,不过,由于程序是单机顺序运行的,所以可以把BFS看成是并行计算的模拟。
在具体编程时,一般用队列这种数据结构来具体实现BFS,甚至可以说“BFS=队列”;
对于DFS,也可以说“DFS=递归”,因为用递归实现DFS是最普遍的。
DFS也可以用“栈”这种数据结构来直接实现,栈和递归在算法思想上是一致的。

BFS

下面用一个图遍历的题目来介绍BFS和队列。我们的目的是遍历图上的所有点以寻找出路。
logo
可以这样走:从起点1出发,走到它所有的邻居2、3;逐一处理每个邻居,例如在邻居2上,再走它的所有邻居4、5、6。继续以上过程,直到所有点都被走到,如上图所示。这是一个“扩散”的过程,如果把搜索空间看成一个池塘,丢一颗石头到起点位置,激起的波浪会一层层扩散到整个空间。需要注意的是,扩散按从近到远的顺序进行,因此,从每个被扩散到的点到起点的路径都是最短的。这个特征对解决迷宫这样的最短路径问题很有用。

用队列来处理这个扩散过程非常清晰、易懂,对照上图:

(a)1进队。当前队列是{1}。
(b)1出队,1的邻居2、3进队。当前队列是{2,3}(可以理解为从1扩散到2、3)。
©2出队,2的邻居4、5、6进队。当前队列是{3,4,5,6}(可以理解为从2扩散到4、5、6)。
(d)3出队,7、8进队。当前队列是{4,5,6,7,8}(可以理解为从3扩散到7、8)。
(e)4出队,9进队。当前队列是{5,6,7,8,9}。

我们用代码实现一下这个过程:

#include<bits/stdc++.h>  
using namespace std;  
char room[4][6] = {  
	"....#",  
	".....",  
	"#....",  
	".#..#",  
};  //地图
int vis[4][6]; //标记数组,用来记录格子是否走过//0:没走过,1:走过了  
int start_x = 2, start_y = 1; //起点坐标  
int hx = 4, hy = 5; //边界  
int dx[4] = {1,0,-1,0};  
int dy[4] = {0,1,0,-1};  
	//dx 和 dy 的每一项对应了一个方向  
	// 例如给一个坐标(x, y)分别加上dx[1]和dy[1],就往下走了一步,得到了(x, y + 1)  
struct node{  
	int x, y;  
	node(int i, int j){x = i, y = j;};  
}; //坐标结构体  
queue<node>que; //记录节点的队列  
bool check(int x, int y){  
	return x>=0 && x<hx && y>=0 && y<hy && vis[x][y]==0 && room[x][y]!='#';  
	//检查这个坐标能不能走(是不是在地图内?之前有没有走过这格?是不是墙壁?)  
}  
void BFS(int sx, int sy){  
	node now = {sx, sy};  
	que.push(now);  
	vis[sx][sy] = 1;  
	while(!que.empty()){  
		now = que.front();  
		que.pop();  
		// cout << now.x << "\t" << now.y << endl; //打印路径上的点  
		for(int i = 0; i < 4; i++){  
			int next_x = now.x + dx[i];  
			int next_y = now.y + dy[i]; //开始向周边探索  
			if(check(next_x, next_y)){  
				vis[next_x][next_y] = 1;  
				node next = {next_x, next_y};  
				que.push(next);  
				//如果这个格子能走,就标记并入列。  
			}  
		}  
	}  
}  
int main(){  
	BFS(start_x, start_y);  
	return 0;  
}

DFS

同上例子,如果我们采用1方法的话,那路径就应该是这样的:

(1)在初始位置令num=1,标记这个位置已经走过。
(2)左、上、右、下4个方向,按顺时针顺序选一个能走的方向,走一步。
(3)在新的位置num++,标记这个位置已经走过。
(4)继续前进,如果无路可走,回退到上一步,换个方向再走。
(5)继续以上过程,直到结束。

在以上过程中,能够访问到所有合法的砖块,并且每个砖块只访问一次,不会重复访问(回退不算重复),如图所示。

为加深对过程的理解,这里给出路径的完整顺序,即:
1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14 → 15

在这个过程中,最重要的特点是在一个位置只要有路,就一直走到最深处,直到无路可走,再退回上一个岔路口,看在上一个岔口的位置能不能换个方向继续往下走。这样就遍历了所有可能走到的位置。

这个思路就是深度搜索:从初始状态出发,下一步可能有多种状态,选其中一个状态深入,到达新的状态,直到无法继续深入,回退到前一步,转移到其他状态,然后再深入下去。最后,遍历完所有可以到达的状态,并得到最终的解。

上述过程用DFS实现是最简单的,代码比BFS短很多。

void DFS(int x, int y){
	vis[x][y] = 1;
	num++;  //记录走了多少步
	//cout << "walk to: " << x << "\t" << y << endl; //打印路径
	for(int i = 0; i < 4; i++){
		int nx = x + dx[i];
		int ny = y + dy[i];
		if(check(nx, ny)){
			DFS(nx, ny);
			//cout << "back to: " << x << "\t" << y << endl; //打印路径
		}
	}
}

状态搜索

搜索的思路不止能用在地图上。搜索更像是从一种状态转变成另一种状态。比如这个例子:
如何输出集合{1,2,3,4,5}的含有三个元素的子集?
这个集合的所有三元素子集为:
1,2,3 // 2,3,4 // 3,4,5 //
1,2,4 // 2,3,5 //
1,2,5 // 2,4,5 //
1,3,4 //
1,3,5 //
1,4,5//
我们可以大致看成如下的一个过程:

上图结合代码更好理解:

#include<bits/stdc++.h>  
using namespace std;  
int nums[6] = {0, 1, 2, 3, 4 ,5};  
int path[4]; //记录每个子集的元素  
void DFS(int now, int cnt){  
	//now 表示当前在 nums 的哪个位置,cnt 记录当前子集里面放了几个元素  
	if(cnt == 4){  
		//思考为什么是等于 4 的时候输出子集?为什么不能是>=4呢?  
		for(int i = 1; i <= 3; i++){  
			cout << path[i] << " ";  
		}  
		cout << endl;  
	}  
	for(int i = now + 1; i <= 5; i++){  
		//从当前位置 now 处继续往后找。  
		path[cnt] = nums[i];  
		DFS(i, cnt + 1);  
	}  
}  
int main(){  
	DFS(0,1);  
	//思考为什么是 0 和 1;  
}

如果不好理解的话,可以看个树图出来辅助理解,树的层数与 cnt 是一致的

1、迷宫(模板题)

给定一个 N × M N \times M N×M 方格的迷宫,迷宫里有 T T T 处障碍,障碍处不可通过。
在迷宫中移动有上下左右四种方式,每次只能移动一个方格。数据保证起点上没有障碍。
给定起点坐标和终点坐标,每个方格最多经过一次,问有多少种从起点坐标到终点坐标的方案。

输入格式

第一行为三个正整数 N , M , T N,M,T N,M,T,分别表示迷宫的长宽和障碍总数。
第二行为四个正整数 S X , S Y , F X , F Y SX,SY,FX,FY SX,SY,FX,FY S X , S Y SX,SY SX,SY 代表起点坐标, F X , F Y FX,FY FX,FY 代表终点坐标。
接下来 T T T 行,每行两个正整数,表示障碍点的坐标。

1 ≤ N , M ≤ 5 1 \le N,M \le 5 1N,M5 1 ≤ T ≤ 10 1 \le T \le 10 1T10 1 ≤ S X , F X ≤ n 1 \le SX,FX \le n 1SX,FXn 1 ≤ S Y , F Y ≤ m 1 \le SY,FY \le m 1SY,FYm

输出格式

输出从起点坐标到终点坐标的方案总数。

样例输入 #1
2 2 1
1 1 2 2
1 2
样例输出 #1
1

answer:

#include<bits/stdc++.h>
using namespace std;
int room[100][100];
//0是路,-1是墙
int hx, hy, sx, sy, ex, ey;
int cnt = 0;
int dx[4] = { 1, -1,0,0 };
int dy[4] = { 0,0,1,-1 };

bool cmp(int x, int y) {
	return x >= 1 && x <= hx && y >= 1 && y <= hy && room[x][y] == 0;
}

void DFS(int x, int y) {
	if (x == ex && y == ey) {
		cnt++;
	}
	room[x][y]++;
	for (int i = 0; i < 4; i++) {
		int nx = x + dx[i];
		int ny = y + dy[i];
		if (cmp(nx, ny)) {
			DFS(nx, ny);
		}
	}
	room[x][y]--;
}

int main() {
	int t;
	cin >> hx >> hy >> t;
	cin >> sx >> sy >> ex >> ey;
	for (int i = 0; i < t; i++) {
		int x, y;
		cin >> x >> y;
		room[x][y] = -1;
	}
	DFS(sx, sy);
	cout << cnt;
}

思考两个地方:

  1. x == ex && y == ye 是什么意思
  2. 为什么room在退回的时候要给路径减一

2、混境之地(模板题)(染色法)

小蓝有一天误入了一个混境之地。好消息是:他误打误撞拿到了一张地图,并从中获取到以下信息:

  1. 混境之地的大小为n·m,其中 ‘#’ 表示不可通过的墙壁,‘ . ’ 表示可以走的路。
  2. 他现在所在位置的坐标为(A,B),而这个混境之地出口的坐标为(C,D),当站在出口时即表示可以逃离混境之地。
  3. 有一句神奇的语可以在混境之地中使用,可以击破面墙壁,即将 ‘#’ 变为‘ . ’。
  4. 神奇的凭语副作用很大,会导致使用者身心俱疲,所以最多只能使用一次。
    小蓝想知道他能否逃离这个混境之地,如果可以逃离这里则输入Yes,反之输出No。
输入格式

第1行输入两个正整数n,m,表示混境之地的大小。
第2行输入四个正整数A,B,C,D,表示小蓝当前所在位置的坐标,以及混境之地出口的坐标。
第3行至第n十2行,每行m个字符,表示混境之地的地图,其中 ‘#’ 表示不可通过的墙壁,‘ . ’ 表示普通的道路。

输出格式

可以逃离混境之地,则输出Yes,反之输出No。

样例输入 #1
5 5 
1 1 5 5 
...#.
..#.. 
#...#
...#.
...#.
样例输出 #1
Yse
样例输入 #2
5 5
1 1 5 5 
...#.
..#..
#...#
...##
..##.
样例输出 #2
No
解释:


左图为样例1,右图为样例2。图1 =可以仅打破一面墙到达终点,而图二则不可以。
我们怎么样才能实现“打破墙壁”呢?
我们可以这样:从起点终点各搜索一次,并给路径涂上不同的颜色,完成后如果起点终点颜色相同,表示不用打破墙壁也可以到达终点;如果颜色不同,就遍历整个地图,对每个墙进行检查,如果某一面墙相邻着两种颜色,则表示打破这个墙就可以到达终点。
answer:

#include<bits/stdc++.h>  
using namespace std;  
const int N = 1e3 + 10;  

char room[N][N];  
int dx[4] = {-1, 0, 1, 0};  
int dy[4] = {0, 1, 0, -1};  
int n, m;  
int A, B, C, D;  
  
bool check(int x, int y){  
	return x > 0 && x <= n && y > 0 && y <= m && room[x][y] == '.';  
}  
bool check2(int x, int y){  
	bool f1 = false, f2 = false;  
	for(int i = 0; i < 4; i++){  
		if(room[x + dx[i]][y + dy[i]] == 'A')f1 = true;  
		if(room[x + dx[i]][y + dy[i]] == 'B')f2 = true;  
	}  
	return f1 && f2;  
}  
  
void dfs(int x, int y, char c){  
	room[x][y] = c;  
	for (int i = 0; i < 4; i++){  
		int nx = x + dx[i], ny = y + dy[i];  
		if(check(nx, ny)){  
			dfs(nx, ny, c);  
		}  
	}  
}  

int main(){  
cin >> n >> m;  
cin >> A >> B >> C >> D;  
	for(int i = 1; i <= n; i++){  
		for(int j = 1; j <= m; j++){  
			cin >> room[i][j];  
		}  
	}  
	dfs(A, B, 'A'); //起点标色为A  
	if(room[A][B] == room[C][D]){  
		cout << "Yes"; return 0;  
	}  
	dfs(C, D, 'B'); //终点标色为B  
	for (int i = 1; i <= n; ++ i ) {  
		for (int j = 1; j <= m; ++j) {  
			if (room[i][j] == '#' && check2(i,j)) {  
				cout << "Yes";  
				return 0;  
			}  
		}  
	}  
	cout <<"No";  
	return 0;  
}

3、马的遍历(BFS)

有一个 n × m n \times m n×m 的棋盘,在某个点 ( x , y ) (x, y) (x,y) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。

输入格式

输入只有一行四个整数,分别为 n , m , x , y n, m, x, y n,m,x,y
1 ≤ x ≤ n ≤ 400 1 \leq x \leq n \leq 400 1xn400 1 ≤ y ≤ m ≤ 400 1 \leq y \leq m \leq 400 1ym400

输出格式

一个 n × m n \times m n×m 的矩阵,代表马到达某个点最少要走几步(不能到达则输出 − 1 -1 1)。

样例输入 #1
3 3 1 1

样例输出 #1

0    3    2    
3    -1   1    
2    1    4

answer:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 4e2+4;
ll room[N][N];
int hx,hy;
bool check(int x, int y){
    return x > 0 && y > 0 && x <= hx && y <= hy && room[x][y] == 0;
}
int dir[][2]={
        {1,2},
        {1,-2},
        {-1,2},
        {-1,-2},
        {2,1},
        {2,-1},
        {-2,1},
        {-2,-1}
    };
struct node{int x, y;};
queue<node>q;
void bfs(int dx, int dy ,ll deep){
    node now, next;
    now = {dx, dy};
    q.push(now);
    while(!q.empty()){
        now = q.front(); q.pop();
        for(int i=0; i<8; i++) {
            next.x = now.x + dir[i][0];
            next.y = now.y + dir[i][1];
            if(check(next.x, next.y)){
                q.push(next);
                room[next.x][next.y] = room[now.x][now.y] + 1;
            }
        }
    }
}
int main(){
    int x,y;
    cin >> hx >> hy >> x >> y;
    bfs(x, y, 0);
    room[x][y] = -1;
    for(int i = 1; i <= hx; i++){
        for(int j = 1; j <= hy; j++){
            if(room[i][j] == -1)cout << 0;
            else if(room[i][j] == 0)cout << -1;
            else cout << room[i][j];
            cout << " ";
        }
        cout << endl;
    }
    return 0;
}

思考为什么不能用DFS。

4、射箭

小明冒充X星球的骑士,进入了一人奇怪的城堡。城堡里边什么都没有,只有方形石头铺成的地面。
假设城堡地面是n×n个方格。如下图所示:

按习俗,骑士要从西北角走到东南角。可以横向或纵向移动,但不能斜着走,也不能跳跃。每走到一个新方格,就要向正北方和正西方各射一箭。(城堡的西墙和北墙内各有几个靶子)同一个方格只允许经过一次。但不必走完所有的方格但是靶子要射完。如果只给出靶子上箭的数目,你能推断出骑士的行走路线吗?有时是可以的,比如上图中的例子。
本题的要求就是已知箭靶数字,求骑士的行走路径

输入描述

第一行一个整数N(0 ≤ N ≤ 20),表示地面有N×N个方格
第二行NV个整数,空格分开,表示北边的箭靶上的数字(自西向东)
第三行N个整数,空格分开,表示西边的箭靶上的数字(自北向南)

输出描述

输出一行若干个整数,表示骑士路径。

为了方便表示,我们约定每个小格子用一个数字代表,从西北角开始,比如,上图中的方块编号为:
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15

输入样例 #1
4
2 4 3 4
4 3 3 3
输出样例 #1
0 4 5 1 2 3 7 11 10 9 13 14 15
#include<bits/stdc++.h>  
using namespace std;  
int noth[25];//y  
int sum_noth = 0, sum_west = 0;  
int west[25];//x  
int room[25][25];  
bool vis[25][25];  
int path[500];  
int cnt = 0;  
int N;  
int dx[] = {0, 0, -1, 1};  
int dy[] = {1, -1, 0, 0};  
  
bool check(int x, int y) {  
	return noth[y]>0 && west[x]>0 && vis[x][y] && x>=0 && x<N && y>=0 && y<N;  
}  
  
void input() {  
	cin >> N;  
	int t = 0;  
	for (int i = 0; i < N; i++) {  
		cin >> noth[i];  
		sum_noth += noth[i];  
	}  
	for (int i = 0; i < N; i++) {  
		cin >> west[i];  
		sum_west += west[i];  
	}  
	for (int i = 0; i < N; i++) {  
		for (int j = 0; j < N; j++) {  
			room[i][j] = t;  
			vis[i][j] = true;  
			t++;  
		}  
	}  
}  
  
void dfs(int x, int y, int step, int flag) {  

	if (x == N - 1 && y == N - 1 && sum_noth == 1 && sum_west == 1) {  
		path[step] = room[x][y];  
		vis[x][y] = false;  
		cnt = step;  
		// cout <<"11111111111111111111"<<"\n";  
		for (int i = 0; i <= step; i++)cout << path[i] << " ";  
		flag = 123;  
	}  
	noth[y] -= 1;  
	west[x] -= 1;  
	sum_west -= 1;  
	sum_noth -= 1;  
	path[step] = room[x][y];  
	vis[x][y] = false;  
	// cout <<"goto : " <<x <<" "<< y <<"\n";  
	for (int i = 0; i < 4; i++) {  
		int nx = x + dx[i];  
		int ny = y + dy[i];  
		if (check(nx, ny))
			dfs(nx, ny, step + 1, flag);  
	}  
	noth[y] += 1;  
	west[x] += 1;  
	sum_west += 1;  
	sum_noth += 1;  
	vis[x][y] = true;  
// cout <<" bcto : " <<x <<" "<< y <<"\n";  
}  
  
int main() {  
input();  
dfs(0, 0, 0, 0);  
}

仔细琢磨DFS部分,以及 sum_noth 和sum_west 的作用,以及path数组的作用。

5、选数(状态搜索)

已知 n n n 个整数 x 1 , x 2 , ⋯   , x n x_1,x_2,\cdots,x_n x1,x2,,xn,以及 1 1 1 个整数 k k k k < n k<n k<n)。从 n n n 个整数中任选 k k k 个整数相加,可分别得到一系列的和。例如当 n = 4 n=4 n=4 k = 3 k=3 k=3 4 4 4 个整数分别为 3 , 7 , 12 , 19 3,7,12,19 3,7,12,19 时,可得全部的组合与它们的和为:
3 + 7 + 12 = 22 3+7+12=22 3+7+12=22
3 + 7 + 19 = 29 3+7+19=29 3+7+19=29
7 + 12 + 19 = 38 7+12+19=38 7+12+19=38
3 + 12 + 19 = 34 3+12+19=34 3+12+19=34
现在,要求你计算出和为素数共有多少种。
例如上例,只有一种的和为素数: 3 + 7 + 19 = 29 3+7+19=29 3+7+19=29

输入格式

第一行两个空格隔开的整数 n , k n,k n,k 1 ≤ n ≤ 20 1 \le n \le 20 1n20 k < n k<n k<n)。
第二行 n n n 个整数,分别为 x 1 , x 2 , ⋯   , x n x_1,x_2,\cdots,x_n x1,x2,,xn 1 ≤ x i ≤ 5 × 1 0 6 1 \le x_i \le 5\times 10^6 1xi5×106)。

输出格式

输出一个整数,表示种类数。

样例输入 #1
4 3
3 7 12 19
样例输出 #1
1

与例题极其相似
answer:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll arr[25];
bool check(ll n){
    if(n == 1)return false;
    for(ll i = 2; i * i <= n; i++){
        if(n % i == 0)return false;
    }
    return true;
}
int ans = 0, n, k;
void dfs(int now, int cnt, ll sum){
    if(cnt == k){
        if(check(sum)){
            ans++;
            //return;
        }
    }
    for(int i = now + 1; i < n; i++){
        dfs(i, cnt + 1, sum + arr[i]);
    }
}

int main(){
    cin >> n >> k;
    for(int i = 0; i < n; i++)cin >> arr[i];
    dfs(-1,0,0);
    cout << ans ;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

fanxinfx2

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

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

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

打赏作者

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

抵扣说明:

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

余额充值