【算法总结】“穷竭”搜索

一、深度优先搜索

POJ No.2386 Lake Counting

Description

Due to recent rains, water has pooled in various places in Farmer John's field, which is represented by a rectangle of N x M (1 <= N <= 100; 1 <= M <= 100) squares. Each square contains either water ('W') or dry land ('.'). Farmer John would like to figure out how many ponds have formed in his field. A pond is a connected set of squares with water in them, where a square is considered adjacent to all eight of its neighbors. 

Given a diagram of Farmer John's field, determine how many ponds he has.
Input

* Line 1: Two space-separated integers: N and M 

* Lines 2..N+1: M characters per line representing one row of Farmer John's field. Each character is either 'W' or '.'. The characters do not have spaces between them.
Output

* Line 1: The number of ponds in Farmer John's field.
Sample Input

10 12
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.
Sample Output

3
Hint

OUTPUT DETAILS: 

There are three ponds: one in the upper left, one in the lower left,and one along the right side.
View Code

  从任意'W'开始,不断将其八连通区域内的'W'换成'.'。则一次DFS后与起始点'W'相连接的所有'W'都被替换为'.',说明此次DFS找到了一个水洼。当图中不再有‘W'时,说明所有水洼都被找到。DFS的次数就等于水洼的个数。

  需要注意的是,scanf("%c", &ch)会将空格、换行符等空字符也作为合法输入,所以需要用 getchar()跳过空字符。

 1 #include <iostream>
 2 #include <vector>
 3 #include <limits.h>
 4 #include <algorithm>
 5 #include <math.h>
 6 using namespace std;
 7 
 8 const int MAX = 100 + 5;
 9 int N, M;
10 char field[MAX][MAX];
11 
12 void dfs(int x, int y) {
13     field[x][y] = '.';
14     // 遍历八连通区域
15     for (int dx = -1; dx <= 1; ++dx) { 
16         for (int dy = -1; dy <= 1; ++dy) {
17             int nx = dx + x, ny = dy + y;
18             if (0 <= nx && nx < N && 0 <= ny && ny < M && field[nx][ny] == 'W')
19                 dfs(nx, ny);
20         }
21     }
22 }
23 
24 int main() {
25     while (scanf("%d%d", &N, &M) != EOF) {
26         for (int i = 0; i < N; ++i) {
27             getchar(); // 为了跳过换行符
28             for (int j = 0; j < M; ++j) {
29                 // scanf("%c", &ch) 会将空格和换行符等空字符作为合法输入
30                 scanf("%c", &field[i][j]);
31             }
32         }
33         int num = 0;
34         for (int i = 0; i < N; ++i) {
35             for (int j = 0; j < M; ++j) {
36                 if (field[i][j] == 'W') {
37                     dfs(i, j);
38                     ++num;
39                 }
40             }
41         }
42 
43         printf("%d
", num);
44     }
45     system("pause");
46     return 0;
47 }
View Code

二、宽度优先搜索

  宽度优先搜索总是先搜索距离初始状态近的状态,开始状态->只需一次转移就可以到达的所有状态->只需2次转移就可以到达的所有状态->。。。对于同一个状态,BFS只经过一次。复杂度为O(状态数$ imes$转移的方式)。

迷宫的最短路径

  给定一个大小为N*M的迷宫,迷宫由通道和墙壁组成,每一步移动可以向邻接的上下左右四格的通道移动。。试求出起点到终点所需的最小步数。(本题假定从起点一定可以移动到终点。'#', ’.’,'S', 'G' 分别表示墙壁、通道、起点和终点)

  宽度优先搜索按照据开始状态由近及远的顺序进行搜索,因此可以很容易地来求最短路径、最少操作之类问题的答案。此题中状态仅仅是目前位置的坐标,因此可以构造为pair或者编码为int来表达状态。当状态更加复杂时,需要封装成一个类来表示状态。转移的方向为4个,状态数与迷宫的大小相等。那么复杂度为$O(4 imes N imes M)=O(N imes M)$。

  宽度优先搜索与深度优先搜索一样,都会生成所有能够遍历到的状态,因此需要对所有状态进行处理时使用宽度优先搜索也是可以的。

  宽度优先搜索中,只要将已经访问过的状态用标记管理起来,就可以很好地做到由近及远的搜索。这个问题中由于要求最短距离,不妨用d[N][M]数组把最短距离保存起来。初始时用充分大的常数INF来初始化它,这样尚未到达的位置就是INF,也就同时起到了标记的作用。虽然到达终点时就会停止搜索,可如果继续下去直到队列为空的话,就可以计算出到各个位置的最短距离。此外,如果搜索到最后,d依然为INF的话,便可得知这个位置就是无法从起点到达的位置。

 1 #include <iostream>
 2 #include <vector>
 3 #include <limits.h>
 4 #include <algorithm>
 5 #include <math.h>
 6 #include <queue>  
 7 using namespace std;
 8 
 9 const int MAX_N = 100 + 5;
10 const int MAX_M = 100 + 5;
11 const int INF = 0x3f3f3f3f; // 常用用法,防止溢出
12 typedef pair<int, int> P;
13 
14 char maze[MAX_N][MAX_M]; // 迷宫数组
15 int N, M;
16 int sx, sy; //起点的位置  
17 int gx, gy; //终点的位置  
18 
19 int d[MAX_N][MAX_M];//到各个位置的最短距离的数组  
20 int dx[4] = { 1, 0, -1, 0 }, dy[4] = { 0, 1, 0, -1 }; // 4个方向
21 
22 void bfs() { // 状态转移次数即距离
23     queue<P> que;
24     for (int i = 0; i < N; i++)
25         for (int j = 0; j < M; j++)
26             d[i][j] = INF;  //初始化起始点到所有点的距离为INF  
27     // 将起始点假如队列,并将这一地点的距离设置为0
28     que.push(P(sx, sy));
29     d[sx][sy] = 0;
30 
31     //不断循环直到队列的长度为0
32     while (que.size()) {
33         P p = que.front(); que.pop();
34         // 已经到达终点则结束搜索
35         if (p.first == gx && p.second == gy)
36             break;
37 
38         for (int i = 0; i < 4; i++) {
39             // 移动后的位置坐标
40             int nx = p.first + dx[i];
41             int ny = p.second + dy[i];
42             //判断是否可移动到该位置以及该位置是否已经被访问过  
43             if (0 <= nx && nx < N && 0 <= ny && ny < M  // 坐标合法,未溢出
44                 && maze[nx][ny] != '#' // 该位置不是障碍
45                 && d[nx][ny] == INF) {//已经被访问过的话不用考虑,因为距离在队列中是递增的 
46 
47                 que.push(P(nx, ny));    //可以移动则加入队列,并且该位置的距离为到p的距离+1  
48                 d[nx][ny] = d[p.first][p.second] + 1;
49             }
50         }
51     }
52 }
53 
54 int main()
55 {
56     scanf("%d%d", &N, &M);
57     for (int i = 0; i < N; ++i) {
58         getchar();
59         for (int j = 0; j < M; ++j) {
60             scanf("%c", &maze[i][j]);
61             if (maze[i][j] == 'S')
62             {
63                 sx = i; sy = j;
64             }
65             if (maze[i][j] == 'G')
66             {
67                 gx = i; gy = j;
68             }
69         }
70     }
71     bfs();
72     printf("%d
", d[gx][gy]);
73 
74     return 0;
75 }
View Code

三、特殊状态枚举   

  虽然生成可行解空间多数采用深度优先搜索,但在状态空间比较特殊时其实可以很简短地实现。比如,C++的标准库中提供了next_permutation这一函数,可以把n个元素共n!种不同的排列生成出来。又或者,通过使用位运算,可以枚举从n个元素中取出k个的共$ extrm{C}_{n}^{k}$种状态或是某个集合中的全部子集等。

四、剪枝

  深度优先搜索时,有时早已很明确地知道从当前状态无论如何转移都不会存在解。这种情况下,不再继续搜索而是直接跳过,这一方法被称作剪枝。

原文地址:https://www.cnblogs.com/Atanisi/p/8711322.html