第三个专题是搜索算法。
搜索算法有很多,比如深度优先搜索(dfs)、广度优先搜索(bfs),A*算法等等。在这一个专题中,我争取让自己能够更加熟悉的掌握dfs和bfs。以及一些剪枝优化。
(1)深度优先搜索
为了求得问题的解,从起始节点开始,找到它的下一个节点,然后沿着这个节点一直找下去,直到没有下一个节点,回溯到其父节点,查看其父节点的其他子节点,如果有,就沿着这个子节点找下去,如果没有就继续回溯。直到将所有节点遍历一遍。
例如: 从A开始:A->B->E->C->F->H->G->D 。这就是一种遍历顺序。
这就是深度优先搜索的搜索过程。
实现:一般用递归的思想或者栈来实现。
迷宫问题,输入一组二维数组用来代表一个迷宫。迷宫中数值为1的表示可走,数值为0的表示不可走。要求求出一条从左上角走到右下角的路径。(大体意思)
实例:poj3984 迷宫问题 (https://vjudge.net/problem/POJ-3984)
本题规定了一个5X5的二维数组,“0”表示可走,“1”表示不可走,要求一条最短路径从(0,0)走到(4,4)。
题解:利用深度优先搜索的方式来解决问题的话,就要利用递归了。
而,递归的关键就是,下一次进入递归的条件和走出递归的条件。
这题递归进入下次的条件是:下一个节点可走并且之前没走过并且没走到终点。
而递归终止的条件就是:走到了终点。
所以本题dfs的解法:
代码:
#include<iostream>
using namespace std;
int map[5][5];
int nx[4]={0,1,0,-1};
int ny[4]={1,0,-1,0};
int ans=1e9;
bool final[5][5];
bool vis[5][5];
int cnt=0;
void dfs(int steps,int x,int y)
{
if(x==4&&y==4) //终止条件,并且记录最短路径
{
if(steps<ans)
{
ans=steps;
for(int i=0;i<5;i++)
for(int j=0;j<5;j++)
final[i][j]=vis[i][j];
}
return ;
}
int xn,yn;
for(int i=0;i<4;i++)
{
xn=x+nx[i];yn=y+ny[i];
if(xn>=0&&xn<=4&&yn>=0&&yn<=4&&!map[xn][yn]&&!vis[xn][yn])
{
vis[xn][yn]=true; //更改为true,不能再走
dfs(steps+1,xn,yn); //递归下去
vis[xn][yn]=false; //为之后记录更短路径
}
}
return ;
}
int main()
{
for(int i=0;i<5;i++)
{
for(int j=0;j<5;j++)
{
cin>>map[i][j];
vis[i][j]=0;
}
}
dfs(0,0,0);
cout<<"(0, 0)"<<endl;
for(int i=0;i<5;i++)
{
for(int j=0;j<5;j++)
{
if(final[i][j])
cout<<"("<<i<<", "<<j<<")"<<endl;
}
}
return 0;
}
要着实注意递归的写法和为了记录最短路径要将vis数组从true再置为false,因为递归的性质:之前走过的走到头的路,不会再走一次。有了这次的理解之后写的代码比我之前的代码精简了很多。
(2)广度优先搜索
广度优先搜索的思想是:从一个节点出发,找到这个节点所能到达的所有节点,记录下来,依次遍历这些节点。并在遍历的过程中,寻找这些节点所能到达的所有节点,记录下来。然后依次遍历这些节点,并查找这些节点所能到达的所有节点,记录下来...一直以这种方式遍历下去,直到所有节点都被遍历到或者找到符合题目要求的节点。
例如: 从A开始遍历顺序:A->B->C->D->E->F->G->H 。
这就是广度优先搜索的过程。
bfs的实现:一般要用到队列来存取节点。如果要输出路径,那么可以用指针来存取中间状态。
注意:bfs实现的路径查找一定是最短的,所以在处理最优路径是一般用bfs来实现。
实例:同样是上面的迷宫问题(poj3984)
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<queue>
using namespace std;
int map[5][5];
int nx[4]={0,1,0,-1};
int ny[4]={1,0,-1,0};
int res;
bool vis[5][5];
struct point
{
int x,y;
//point(int n,int m):x(n),y(m)
//{ };
point *last;
};
point* bfs()
{
queue<point *>q;
point *ans=new point;
ans->x=0,ans->y=0;
ans->last=NULL;
q.push(ans);
vis[0][0]=true;
while(!q.empty())
{
ans=q.front();q.pop();
if(ans->x==4&&ans->y==4) return ans;
int xn,yn;
for(int i=0;i<4;i++)
{
xn=ans->x+nx[i];yn=ans->y+ny[i];
if(xn>=0&&xn<=4&&yn>=0&&yn<=4&&!map[xn][yn]&&!vis[xn][yn])
{
point *e = new point;
e->x=xn,e->y=yn;
e->last=ans;
q.push(e);
vis[xn][yn]=true;
}
}
}
return NULL;
}
void output(point *s)
{
if(s==NULL) return ;
output(s->last);
cout<<"("<<s->x<<", "<<s->y<<")"<<endl;
}
int main()
{
for(int i=0;i<5;i++)
{
for(int j=0;j<5;j++)
{
cin>>map[i][j];
vis[i][j]=0;
}
}
output(bfs());
return 0;
}
输出过程(指针应用)参考与:https://blog.csdn.net/acmer_sly/article/details/52492245
以上是dfs和bfs的基础的应用。而dfs又有优化的方式:剪枝优化(减去中间过程中一定不会到达目标的中间状态)。
剪枝优化:
例如: 在判断是否符合条件之后添加另外一个判断。
if(steps>ans) return ;
这样就会使得在找到一个路径之后,ans被赋值之后,在之后的dfs寻找中,如果步数大于了之前找过的ans的步数。那么这样的多余步骤就可以舍弃。因为我们要求找到最短路径,如果步数大于了已经找到的一个完整路径的步数,我们就没必要再考虑这条新的路径了。
另一个剪枝:如果当前步骤时,当前搜索点的位置加上它最短到达终点的步骤数都要比之前一个结果ans大,那么这条路径也不用考虑了。代码如下:
if((steps+abs(x-4)+abs(y-4))>ans) return ;
(3)A*算法
A*搜索算法,不赘述。主要的思想就是利用一个标准来判断每次步骤的移动。这个标准就是F,F=G+H。G表示从起点到当前位置移动所耗费的代价。H表示从当前位置到目标位置的最优路径的耗费。这两个耗费相加得到的F作为判断下一步的移动情况。最终到达目标位置。再利用指针遍历最短路径。
推荐一篇文档,我是看这篇看明白A*算法的:https://blog.csdn.net/hitwhylz/article/details/23089415
另外搜索的经典问题:八皇后问题。
博客上有很多详细的八皇后问题的解答我在这里也不赘述,挑了一些我看懂的博客:
第一篇并没有用到递归。但是while和if等循环判断条件运用的很完美,通过循环判断,完成一次可行解的寻找。通过回溯的方法继续寻找下一个解。代码很是精炼,仔细看能看明白。
https://blog.csdn.net/qq_326324545/article/details/80919368
第二篇有非递归算法和递归算法。我建议看看递归算法,和上面那个几乎一样的思路,但是用到了递归。代码更加精简。
https://www.cnblogs.com/yjd_hycf_space/p/6670316.html
好了,搜索算法就写到这儿吧,期末考试快来了,真没多少时间了~~