一、深度优先搜索
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.
从任意'W'开始,不断将其八连通区域内的'W'换成'.'。则一次DFS后与起始点'W'相连接的所有'W'都被替换为'.',说明此次DFS找到了一个水洼。当图中不再有‘W'时,说明所有水洼都被找到。DFS的次数就等于水洼的个数。
需要注意的是,scanf("%c", &ch)会将空格、换行符等空字符也作为合法输入,所以需要用 getchar()跳过空字符。
1 #include2 #include 3 #include 4 #include 5 #include 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\n", num); 44 } 45 system("pause"); 46 return 0; 47 }
二、宽度优先搜索
宽度优先搜索总是先搜索距离初始状态近的状态,开始状态->只需一次转移就可以到达的所有状态->只需2次转移就可以到达的所有状态->。。。对于同一个状态,BFS只经过一次。复杂度为O(状态数$\times$转移的方式)。
迷宫的最短路径
给定一个大小为N*M的迷宫,迷宫由通道和墙壁组成,每一步移动可以向邻接的上下左右四格的通道移动。。试求出起点到终点所需的最小步数。(本题假定从起点一定可以移动到终点。'#', ’.’,'S', 'G' 分别表示墙壁、通道、起点和终点)
宽度优先搜索按照据开始状态由近及远的顺序进行搜索,因此可以很容易地来求最短路径、最少操作之类问题的答案。此题中状态仅仅是目前位置的坐标,因此可以构造为pair或者编码为int来表达状态。当状态更加复杂时,需要封装成一个类来表示状态。转移的方向为4个,状态数与迷宫的大小相等。那么复杂度为$O(4\times N\times M)=O(N\times M)$。
宽度优先搜索与深度优先搜索一样,都会生成所有能够遍历到的状态,因此需要对所有状态进行处理时使用宽度优先搜索也是可以的。
宽度优先搜索中,只要将已经访问过的状态用标记管理起来,就可以很好地做到由近及远的搜索。这个问题中由于要求最短距离,不妨用d[N][M]数组把最短距离保存起来。初始时用充分大的常数INF来初始化它,这样尚未到达的位置就是INF,也就同时起到了标记的作用。虽然到达终点时就会停止搜索,可如果继续下去直到队列为空的话,就可以计算出到各个位置的最短距离。此外,如果搜索到最后,d依然为INF的话,便可得知这个位置就是无法从起点到达的位置。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 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 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\n", d[gx][gy]); 73 74 return 0; 75 }
三、特殊状态枚举
虽然生成可行解空间多数采用深度优先搜索,但在状态空间比较特殊时其实可以很简短地实现。比如,C++的标准库中提供了next_permutation这一函数,可以把n个元素共n!种不同的排列生成出来。又或者,通过使用位运算,可以枚举从n个元素中取出k个的共$\textrm{C}_{n}^{k}$种状态或是某个集合中的全部子集等。
四、剪枝
深度优先搜索时,有时早已很明确地知道从当前状态无论如何转移都不会存在解。这种情况下,不再继续搜索而是直接跳过,这一方法被称作剪枝。