思考一个迷宫问题:有一个错综复杂的迷宫,让一堆老鼠寻找迷宫的出口,老鼠应该怎么办?
- 一只老鼠走迷宫。它在每个路口都选择先走右边(先走左边也可以),能走多远就走多远,直到碰壁无法继续往前走,然后回退一步,这一次走左边,接着继续往下走。用这个办法能走遍所有的路,而且不会重复(这里规定回退不算重复走)。这个思路就是DFS(深度优先搜索)。
- 一群老鼠走迷宫。假设老鼠是无限多的,这群老鼠进去后,在每个路口派出部分老鼠探索所有没走过的路。走某条路的老鼠,如果碰壁无法前行,就停下;如果到达的路口已经有其他老鼠探索过了,也停下。很显然,所有的道路都会走到,而且不会重复。这个思路就是BFS(广度优先搜索)。
BFS看起来像“并行计算”,不过,由于程序是单机顺序运行的,所以可以把BFS看成是并行计算的模拟。
在具体编程时,一般用队列这种数据结构来具体实现BFS,甚至可以说“BFS=队列”;
对于DFS,也可以说“DFS=递归”,因为用递归实现DFS是最普遍的。
DFS也可以用“栈”这种数据结构来直接实现,栈和递归在算法思想上是一致的。
BFS
下面用一个图遍历的题目来介绍BFS和队列。我们的目的是遍历图上的所有点以寻找出路。
可以这样走:从起点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 1≤N,M≤5, 1 ≤ T ≤ 10 1 \le T \le 10 1≤T≤10, 1 ≤ S X , F X ≤ n 1 \le SX,FX \le n 1≤SX,FX≤n, 1 ≤ S Y , F Y ≤ m 1 \le SY,FY \le m 1≤SY,FY≤m。
输出格式
输出从起点坐标到终点坐标的方案总数。
样例输入 #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;
}
思考两个地方:
- x == ex && y == ye 是什么意思
- 为什么room在退回的时候要给路径减一
2、混境之地(模板题)(染色法)
小蓝有一天误入了一个混境之地。好消息是:他误打误撞拿到了一张地图,并从中获取到以下信息:
- 混境之地的大小为n·m,其中 ‘#’ 表示不可通过的墙壁,‘ . ’ 表示可以走的路。
- 他现在所在位置的坐标为(A,B),而这个混境之地出口的坐标为(C,D),当站在出口时即表示可以逃离混境之地。
- 有一句神奇的语可以在混境之地中使用,可以击破面墙壁,即将 ‘#’ 变为‘ . ’。
- 神奇的凭语副作用很大,会导致使用者身心俱疲,所以最多只能使用一次。
小蓝想知道他能否逃离这个混境之地,如果可以逃离这里则输入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
1≤x≤n≤400,
1
≤
y
≤
m
≤
400
1 \leq y \leq m \leq 400
1≤y≤m≤400。
输出格式
一个 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
1≤n≤20,
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
1≤xi≤5×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 ;
}