《啊哈算法》——搜索
这篇文章我们将通过一些实例来初步理解两种搜索算法:dfs、bfs。
按照惯例,我们依然首先给出一个具体问题来导入:给出一个n x m的矩阵迷宫map,人在迷宫上的某个点map[i][j],可以上下左右移动,但是一些点标记为不可经过。那么现在给出起点和终点的矩阵下的坐标,我们能否找到一条起点到终点的路径?需要移动多少步?
深度优先搜索:
我们先通过之前一个我们曾经见到过的较为简单的“生成全排列”问题来初步认识一下dfs的思想。
我们将问题更加的形象化,即我们假设生成全排列是一个将1~n个数字放到A~N个桶里的过程,这里以n=4为例吧,假设一次我们放数字的过程得到了2143这个全排列,在将3放入D盒中,我们期望能够继续生成其余的全排列,于是我们将3从D中拿出来,看有没有其余选择,发现并没有得到另外一种全排列的可能性,那么我们再将4从C盒中取出,这时,我们便可以将3放入C中,4放入D中来得到一个新的全排列。
上面其实描述了一个简单的回溯过程,我们更加抽象的来理解dfs过程,即它将一个问题分成n个步骤,第i个步骤有a[i]个选择,那么我们在完成第i个步骤时,先任选a[i]中的一个,随后开始第i+1个步骤,一直到第n个步骤,我们枚举玩完第n个步骤的a[n]个方法后,开始向上回溯,即我们在进行第n-1个步骤有a[n-1]-1个没有遍历到的方法当中的一个,然后再进行第n个步骤,枚举a[n]中方法,很显然,这种算法能够搜索到完成这个问题所采取的n个步骤的不同方法组合的所有情况。
那么针对生成全排列,我们尝试用dfs的思路进行重写。
简单的参考代码如下。
#include<cstdio> using namespace std; int a[10] , book[10] , n; void dfs(int step)//给第step箱子填充数字 { int i; if(step == n + 1)//生成了一种全排列,输出结果 { for(i = 1;i <= n;i++) printf("%d",a[i]); printf("\n"); return; } for(i = 1;i <= n;i++)//当前情况的方法 { if(book[i] == 0) { a[step] = i;//选择一种方法 book[i] = 1;//标记这个数字已经使用过 dfs(step + 1);//进行深度优先,即开始给第step + 1个箱子填数 book[i] = 0;//回溯到填充第step箱子的步骤,清除标记 } } return; } int main() { scanf("%d",&n); dfs(1); }
那么基于这层铺垫,我们来看一看如何用dfs处理我们在文章一开始提出的问题。
我们从起点开始,每一步其实有四个选择,即上、下、左、右,当然这里排除走出边界的、有障碍物的情况,假设这里我们从map[sx][sy]开始,(map是用于储存图的邻接矩阵)每当走了一步,假设我们向右走了一步,到达map[i][j+1],我们深度优先,继续从map[i][j+1]开始继续往下走...直到走到某个点map[i'][j'],发现四处无路可走,那么便开始回溯到路径中上一个点,去走那些没有走过的方向。这里需要注意的一个问题是,我们从map[i][j]走到了map[i][j+1],那么再从map[i][j+1]开始走的时候,会面临又回到map[i][j]的情况,这里只需在深搜的时候标记已经走过的点便可以轻松处理了。
简单的参考代码如下。
#include<cstdio> using namespace std; int n,m,p,q,Min = 9999999; int a[51][51],book[51][51]; void dfs(int x,int y,int step) { int next[4][2] = {{0,1},{1,0},{0,-1},{-1,0}}; int tx,ty,k; if(x == p && y == q) { if(step < Min) Min = step; return; } for(k=0;k<=3;k++)//四个方向 { tx=x+next[k][0]; ty=y+next[k][1]; if(tx < 1 || tx>n ||ty<1||ty>m)//越界 continue; if(a[tx][ty]==0&&book[tx][ty] == 0) { book[tx][ty] = 1; //标记访问过 dfs(tx,ty,step+1);//深度优先的搜索 book[tx][ty] = 0; } } return; } int main() { int i , j , startx , starty; scanf("%d%d",&n,&m); for(i = 1;i <= n;i++) for(j=1;j<=m;j++) scanf("%d",&a[i][j]); scanf("%d %d %d %d",&startx,&starty,&p,&q); book[startx][starty] = 1; dfs(startx,starty,0); printf("%d",Min); }
宽度优先搜索:
其实通过前面对深度优先搜索的介绍,这里我们对比来看就很容易理解宽度优先搜索的思路。
简单的说,假设我们完成搜索需要进行n个步骤,记第i个步骤有a[i]个方法。
深搜给出的思路是,先使用a[i]给出的完成第i个步骤的一个方法,然后紧接着去完成第i+1个步骤,对于那些没有用到的方法,深搜在搜到底部之后没有其余方法后,会回溯回来再重新a[i]中的其余方法,以此来构造出新的方法。
而宽搜给出的思路是,完成第i个步骤,即将a[i]种方法全部列出来,这显示出当前(进行i个步骤)的所有情况,然后进行第i+1个步骤,采用相同的策略。一直到第n个步骤,宽搜将列举出所有的可能情况,然后完成搜索。
那么我们现在面临的一个问题,如何编码来实现宽搜的这一系列操作呢?我们可以看到,我们在完成对第i个步骤的a[i]种情况的列举之后,之前i-1个步骤所出现的情况似乎已经与我们没有关系了,因为我们在进行第i+1个步骤的时候,仅仅需要基于完成第i个步骤后所有结果即可。想一想,以这种尾部添加元素,头部删除元素的操作......对,就是我们在前面文章曾经介绍过的队列结构。
基于这种宽搜过程,我们只需判断第j个步骤之后,是否出现我们想要的结果。可以看到,每宽搜一次,都要删除头部元素,可能添加尾部元素。而当头部元素一直删除而尾部却没有添加多少,即队列为空的时候,依然没有出现我们想要的结果,那么这显然表明我们在图中找不到这样一个从起点到达终点的路径。
基于上面的算法分析,我们进行简单的编程实现。
#include<cstdio> using namespace std; struct note { int x; int y; int f; int s; }; int main() { struct note que[2501]; int a[51][51] = {0},book[51][51] = {0}; int next[4][2] = {{0,1}, //四个方向 {1,0}, {0,-1}, {-1,0}}; int head , tail; int i , j , k , n , m , startx , starty , p , q , tx , ty , flag; scanf("%d %d",&n,&m); for(i = 1;i <= n;i++)//读入map for(j = 1;j <= m;j++) scanf("%d",&a[i][j]); scanf("%d%d%d%d",&startx,&starty,&p,&q); head = 1; tail = 1;//初始化队列 que[tail].x = startx; que[tail].y = starty; que[tail].f = 0; que[tail].s = 0; tail++; book[startx][starty] = 1; while(head < tail) //bfd算法核心部分 { for(k = 0;k <= 3;k++) { tx = que[head].x + next[k][0]; ty = que[head].y + next[k][1]; if(tx < 1 || tx > n || ty < 1 || ty > m) continue; if(a[tx][ty] == 0 && book[tx][ty] == 0)//入队操作 { book[i][j] = 1; que[tail].x = tx; que[tail].y = ty; que[tail].f = head; que[tail].s = que[head].s + 1; tail++; } if(tx == p && ty == q) { flag = 1; break; } } if(flag == 1) break; head++; //弹出队首元素 } printf("%d",que[tail-1].s); return 0; }