深度优先搜索(DFS)
DFS(Depth-First Search)是搜索的手段之一。
它从某一个状态开始,不断地转移状态直到无法转移,然后退回到前一步地状态(很容易想到,要用递归实现),继续转移到其他状态,如此不断重复,直到找到最终的解。
比如求解数独,首先在某个格子内填入适当的数字,然后再继续在下一个格子内填入数字,如此重复下去。如果发现某个格子无解了,就放弃前一个格子上选择的数字,改用其他可行的数字。
下面的这个二叉树的遍历过程可以让你更加了解。
1->2->3->4–3->5–3--2->6–2--1->7->8->9–8->10–8--7->11
–抽象地表示返回前一状态
下面使用DFS解决一个问题:
部分和问题
给定整数a1,a2,a3,a4, …,an,判断是否可以从中选出若干数,使它们的和恰好为k.
limits:
1≤n≤20
-1e8≤n≤1e8
-1e8≤n≤1e8样例1
Input:
n=4
a={1,2,4,7}
k=13
Output:
Yes (13 = 2 + 4 + 7)
样例2
Input:
n=4
a={1,2,4,7}
k=15
Output:
No
从a1开始按顺序决定每个数加或者不加,在全部的n个数都决定后在判断它们的和是不是k即可。因为状态数是2的n+1次幂,所以复杂度为O(2^n)。如何实现这个搜索,请参考下面的代码。注意a的下标与题目描述的下标偏移了1。
//input
int a[MAX_N];
int n,k;
//已经从前i项得到了和sum,然后对i项之后的进行分支
bool dfs(int i, int sum){
//如果前n项都计算过了,则返回sum是否与k相等
if(i == n) return sum == k;
//不加上a[i]的情况
if(dfs(i + 1, sum)) return true;
//加上a[i]的情况
if(dfs(i + 1, sum + a[i])) return true;
//无论是否加上a[i]都不能凑成k就返回false
return false;
}
void solve(){
if(dfs(0, 0)) printf("Yes\n");
else printf("No\n");
}
深度优先搜索从最开始的状态出发,遍历所有可以到达的状态。由此可以对所有的状态进行操作,或者列举出所有的状态。
下面再来一个POJ的题目:
Lake Counting (POJ No.2386)
有一个大小为N x M 的园子,雨后积了水。八连通的积水被认为是连接在一起的。请求出园子里总共有多少水洼?(八连通指的是下图中相对W的*的部分)
***
*W*
***
限制条件:
N,M<=100样例
输入:
N=10, M=12 (‘W’表示积水,’.'表示没有积水)
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.
输出:
3
从任意的W开始,不停地把邻接的部分用’.'代替。1次DFS后与初始的这个W连接的所有的W就都被替代为.
。因此直到图中不再存在W为止,总共进行DFS的次数就是答案了。8个方向共对应了8种状态转移,每个格子作为DFS的参数至多被调用一次,所以复杂度为O(8 x N x m) = O(N x M)。
//输入
int N, M;
char field[MAX_N][MAX_M+1]; //园子
//现在位置(x,y)
void dfs(int x, int y){
//将现在所在的位置替换为.
field[x][y] = '.';
//循环遍历移动的8个方向
for(int dx = -1; dx <= 1; dx++){
for(int dy = -1; dy <= 1; dy++){
//向x方向移动dx,向y方向移动dy,移动的结果为(nx,ny)
int nx = x + dx, ny = y + dy;
//判断(nx,ny)是不是在园子里,以及是否有积水
if(0 <= nx && nx <N && 0 <= ny && ny < M && field[nx][ny] == 'W') dfs(nx, ny);
}
}
return ;
}
void solve(){
int res = 0;
for(int i = 0; i < N; i++){
for(int j = 0; j < M; j++){
//从有W的地方开始DFS
if(field[i][j] == 'W'){
dfs(i, j);
res++;
}
}
}
}
下面再来一个例子让你更加了解DFS
迷宫问题
给出一个n*m的迷图,一个入口,一个出口。
编写程序打印一条从入口到出口的路径。
-1表示不通。
0表示可以走。
只能往上、下、左、右这四个方向走。如果没有路则输出"no way."。
样例:
输入:
8 5
-1 -1 -1 -1 -1
0 0 0 0 1
-1 -1 -1 0 -1
-1 0 0 0 -1
-1 0 0 1 -1
-1 0 0 0 -1
-1 -1 -1 0 -1
-1 0 0 0 -1
输出:
…
…
…
…
代码:(完整)
#include <iostream>
using namespace std;
int n,m,desx,desy,soux,souy,totstep,a[51],b[51],map[51][51];
bool f;
int move(int x, int y, int step)
{
map[x][y] = step; //走一步,作标记,把步数记下来
a[step]=x; //保存路径,为了后续的输出
b[step]=y;
if((x==desx)&&(y==desy))
{
f=1;
totstep=step;
}
else
{
if((y!=m)&&(map[x][y+1]==0)) move(x,y+1,step+1);//向右
if((!f)&&(x!=n)&&(map[x+1][y]==0)) move(x+1,y,step+1);
//向下
if((!f)&&(y!=1)&&(map[x][y-1]==0)) move(x,y-1,step+1);
//向左
if((!f)&&(x!=1)&&(map[x-2][y]==0)) move(x-1,y,step=1);
//向上
}
}
int main()
{
int i,j;
cin>>n>>m; //n行m列
for(i=1;i<=n;i++) //读入迷宫,0表示通,-1表示不通
for(j=1;j<=m;j++)
cin>>map[i][j];
cout<<"Input the enter:";
cin>>soux>>souy; //入口
cout<<"Input the exit:";
cin>>desx>>desy; //出口
f=0; //f=0表示无解。f=1表示找到一个解
move(soux,souy,1);
if(f)
{
for(i=1;i<=totstep;i++) //输出走迷宫的路径
cout<<a[i]<<","<<b[i]<<endl;
}
else cout<<"no way."<<endl;
return 0;
}
上面有个要注意的就是上下左右的优先选择。
以入口在左上,出口在右下,为例(默认找到时退出,在下面的过程中就省略不写了)
已经访问过的节点会被作标记,看代码map[x][y] = step;
这里给了那个节点一个不为0的值,既认为该节点不通。
先一直向左移,直到不通或者到边缘。然后再向下。直到不通或到边缘。然后再向左,然后再向上。
想象一下,如果还有上面的那个模式,但是出口和入口换了一个位置,那么我们向左和向下的这个过程,其实对我们求解这个问题是没有帮助的(虽然让我们排除了一定的范围,但是在测试的环境中,我们已经知道了出口和入口的位置了)。
策略的选择影响效率。
还有,这个一般都是先判断再移动。而不是像上面那个二叉树那样还要回溯(先移动,再判断,符合继续移动,不符合则要返回上一个节点)。