写在前面:搜索问题,最基础的就是深搜和广搜,也就是DFS和BFS,本文只讨论范围就仅限此两种方法。
概念
1.DFS
深度优先搜索,字面意思是纵向,往下搜索。引用挑战程序设计竞赛里的描述:它从某个状态开始,不断地转移状态直到无法转移,然后回退到前一步的状态,继续转移到其他状态,如此不断重复,直至找到最终的解。
请观察下图
上图是用深度搜索,遍历所有点的演示。实心表示遇到的新点,圆圈内数字表示每个点被遍历的顺序,橙色表示回退前一步。根据题目的不同,在遍历所有点的过程中,有条件的进行匹配。
2.BFS
广度优先搜索,(BFS, Breadth-First Search)是一种发散式的搜索,横向搜索。引用挑战程序设计竞赛里的描述:它与深度优先搜索类似,从某个状态出发探索所有可以到达的状态。与DFS不同在于搜索的顺序,BFS总是先搜索距离初始状态近的状态。也就是说,它是按照开始状态--->只需1次转移就可以到达的所有状态--->只需2次转移就可以到达的所有状态--->......这样的顺序进行搜索。对于同一个状态,宽度优先搜索只经过一次,因此复杂度为O(状态数*转移的方式)
请观察下图
从图2可以知道,BFS进行遍历时,是从上至下一层一层进行搜索的。深色表示遇到遍历的新点,橙色表示对此点可以到达的其他状态进行遍历。明确看出,BFS对于同一个状态只经过了一次。
实现
对于DFS,一般采用递归方式,隐式地利用栈进行计算。而BFS则采用队列的方式,先进先出,先进先遍历,将初始状态的添加到队列,此后不断地在队列最前端取出状态,把从改状态可以转移到的状态中尚未访问过的部分加入队列,如此反复,直到队列被取空或找到了问题的解[摘自挑战程序设计竞赛]。
DFS耗费空间比BFS小,因为DFS是一种深度的搜索,复杂度与空间深度成正比;而BFS是通过遍历每个状态进行搜索,复杂度与状态数有关。可想而知状态数必定大过递归的深度,所以DFS比BFS更加节省空间。
但对于寻找最短路的问题,BFS不会反复进过同一个状态,而DFS则会如此,那么在这类问题中,BFS比DFS更适合解决。
BFS不一定总比DFS快,这是根据具体问题具体分析的。下面我们将引用两道例题,来更详细对比两者的不同。
例题1( 棋盘问题POJ--1321 )
描述
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。
Input
输入含有多组测试数据。
每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n
当为-1 -1时表示输入结束。
随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。
Output
对于每一组数据,给出一行输出,输出摆放的方案数目C (数据保证C<2^31)。
Sample Input
2 1
#.
.#
4 4
...#
..#.
.#..
#...
-1 -1
Sample Output
2
1
题目分析
题目很明确,就是寻找有多少种可行的摆放方式,即路径。那么这一条路有什么条件限制呢?1、一定要将所有棋子落下(棋子数保证不会比格子数大) ;2、同一行或者同一列只能有一个棋子。
解题思路
寻找路径,但不是最短的,只要符合条件的路即可,所以必定需要不断遍历整个棋盘,以寻找可行的摆放方式。那么我们可以采用DFS或BFS,但在这我们用DFS,因为可以让思路更加明确。在进行DFS的过程中,加入题目条件即可。1、退出条件:当所有棋子落下或棋盘无法继续遍历;2、棋子只能落在可以放的格子,以及这个格子所在行或列没有其他棋子占有。
其中对于条件2,需要再进行思考,如何判断这个格子所在行或列是否被其他棋子占有呢?首先解决对行的观察,我们可以逐行深搜,先对一行的所有点遍历,找到了可以落的格子,就再进行下一行的DFS,如果无法找到路径就返回上一行继续搜索,这样就保证每行只有一个点。然后对于列,我们则需要设置标记,给每列设置一个标记,如果此列被占就更改标记,以后就只要观察标记就好了。
AC代码
#include <iostream>
#include <string.h>
using namespace std;
int n,k;
int maxway; //总方案数
int dir[9][9]; //用矩阵储存棋盘,dir[0]是用作标记存储
int maxroad;
void dfs(int k,int x){
if(x > n+1) //如果超过棋盘边线,则退出
return;
if(k <= 0){ //如果将所有棋子落下,则退出,答案+1
maxway++;
return;
}
for(int j = x; j <= n; j++){ //遍历第X行的所有格子(因为行是逐层遍历,所以不需要给格子所占行进行标记)
for(int i = 1; i <= n; i++){
if(dir[0][i] == 0 && dir[j][i] == 1){ //如果这个格子所占的列没被占,并且有空位,则放棋子
dir[0][i] = 1; //给所占列进行标记
dfs(k-1, j+1); //继续遍历下一行
dir[0][i] = 0; //如果没法放下所有棋子,则回退,所占列的标记清零
}
}
}
return;
}
int main(){
while(1){
cin >> n >> k;
if(n == -1 && k == -1)
break;
memset(dir, 0, sizeof(dir));
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
char k;
cin >> k;
if(k == '#')
dir[i][j] = 1;
else
dir[i][j] = 0;
}
}
maxway = 0;
dfs(k,1);
cout << maxway << endl;
}
return 0;
}
例题2(POJ--3278)
描述
Farmer John has been informed of the location of a fugitive cow and wants to catch her immediately. He starts at a point N (0 ≤ N ≤ 100,000) on a number line and the cow is at a point K (0 ≤ K ≤ 100,000) on the same number line. Farmer John has two modes of transportation: walking and teleporting.
* Walking: FJ can move from any point X to the points X - 1 or X + 1 in a single minute
* Teleporting: FJ can move from any point X to the point 2 × X in a single minute.
If the cow, unaware of its pursuit, does not move at all, how long does it take for Farmer John to retrieve it?
Input
Line 1: Two space-separated integers: N and K
Output
Line 1: The least amount of time, in minutes, it takes for Farmer John to catch the fugitive cow.
Sample Input
5 17
Sample Output
4
Hint
The fastest way for Farmer John to reach the fugitive cow is to move along the following path: 5-10-9-18-17, which takes 4 minutes.
题目分析
问题描述是讲有一个农夫,要去抓住他的牛,规定两者都处于一条直线上。农夫在位置N,牛在位置K。农夫只能左右移动,共有两种移动方式:1、向左走一步或向右走一步;2、向右移动2倍的距离(比如:农夫在5,那他可以移动到10,5*2=10)
然后规定了,这条直线有两个端点0和100000,所以农夫和牛的位置范围也在这条直线内。最后需要我们求出农夫抓到牛,最短需要多少时间,移动一次算1min,也就是最少的步数。
解题思路
虽然农夫的移动是在一条直线上,但因为移动的方式不同,所在的位置也不同,所以其实抽象来看是一个三叉树。在每个位置都有三种移动方式:向左一步、向右一步、向右跨2倍距离。最后退出条件,就是农夫的位置和牛的位置一样 。最后题目要求最短的时间,即最短的步数。那么就非常符合BFS的特点。
AC代码
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 100000;
int N,K;
int visited[MAXN+10]; //标记数组
struct Step{
int x;
int steps;
Step(int xx, int s): x(xx),steps(s){}
};
void BFS(queue<Step> q){
while(!q.empty()){ //若队列被取空,则退出
Step s = q.front(); //不断取最前端的状态
if(s.x == K){ //如果此时的位置是牛的位置,则退出
cout << s.steps << endl; //输出步数
break;
}
else{
if(s.x+1 >= 0 && !visited[s.x+1]){ //如果往右走一步没越界,并且这个点没有走过
q.push(Step(s.x+1,s.steps+1)); //将此状态装入队列
visited[s.x+1] = 1; //更改点的标记
}
if(s.x-1 >= 0 && !visited[s.x-1]){ //如果往左走一步没越界,并且这个点没走过
q.push(Step(s.x-1,s.steps+1));
visited[s.x-1] = 1;
}
if(s.x*2 <= MAXN && !visited[s.x*2]){ //如果往右跨2倍的距离没越界,并且这个点没有走过
q.push(Step(s.x*2,s.steps+1));
visited[s.x*2] = 1;
}
q.pop(); //将此状态弹出队列
}
}
}
int main(){
cin >> N >> K;
queue<Step> q;
memset(visited, 0, sizeof(visited));
q.push(Step(N,0)); //从点N出发,步数为0
visited[N] = 1; //出发点标记一下
BFS(q);
return 0;
}