神奇的搜索--深度优先搜索

写这篇文章也是在讲课的时候临危受命,因此只能写一点是一点,今天先把深度优先搜索写了,下次有时间再补一下广度优先搜索

首先,什么是深度优先?什么是广度优先?

所谓深度优先,是指在一棵搜索树上(抽象的树)沿着某条路径一直往下搜索,直到不能再进行,才更换搜索路径,并一直往下搜索至无法搜索为止,有点"不撞南墙不回头"的意思,依次执行直到遍历整个搜索树或者是搜索到结果则退出,可以类比二叉树的先序遍历过程

所谓广度优先,是指在一棵搜索树上(抽象的树)一层一层地搜索,直到搜索完整个搜索树或者搜索到结果则退出,广度优先搜索可以类比二叉树的层序遍历过程

通过下面的例子,读者应该能很好地理解这两种搜索策略的思想:

 下面我们通过两个实例,来说明DFS算法的具体思路:

1.数字的全排列问题

我们知道对于数字123,其全排列为:123,132,213,231,312,321这6种,那么我们能否通过程序实现全排列呢?当然是可以的,我们可以通过DFS来实现它,具体我们以12345的全排列为例:

首先我们给个代码:

#include<iostream>
#include<cstring>
using namespace std;

bool vis[10];

void dfs(int a[],int cnt,int len)
{   //函数第一次执行前,将vis数组全部标记为false;
    if(cnt == len)
    {
        for(int i = 0; i < len; i++)
            cout<<a[i];
        cout<<endl;
        return;
    }
    for(int i = 1; i <= len; i++)
    {
        if(!vis[i])
        {
            vis[i] = 1;
            a[cnt] = i;
            dfs(a,cnt+1,len);
            vis[i] = 0;
        }
    }
}

int main()
{
    int a[10];
    memset(vis,0,sizeof vis);
    dfs(a,0,5);
    return 0;
}

从代码分析思路: 首先我们全排列第一个数字,是通过枚举得到的for(i from 1 to len) -> i,而cnt的作用是计数,用于表示当前我们确定了多少个数字,比如对12345这5个数字全排列,我们确定了5个数字即得到了一组排列,a[]数组用来暂存排列的结果,当cnt == len,即确定一组排列时,这组排列就存在a[]中,因此输出即可,vis[]数组呢?这是搜索的关键部分了,vis[i] == 0,表示i没有被使用过,否则表示i被使用过,不能再用了.这就是说,全排列中不存在11234这样的,因为1不能够出现两次(用过不能再用原则),因此当vis[i] == 0时,才有继续搜索下去的必要,但是我们会发现,vis[i] == 0时,先被置为1,递归回溯时,又被置为0了,这是为什么呢?因为我们回溯时要释放对i的控制权,以便i能够在以后的搜索中被利用.比如:12345 -> 12354的过程中,4和5先被占用,后被释放,因此才能够重新利用它们使得状态由12345变为12354.我们可以画一棵不太完整的搜索树:

其中每个节点的后继都应该是同样的子树,这里只画了一部分,带红×的路径表示无效路径

可以知道,搜索到第5层的时候,所有的路径均是无效的,此时应该回溯,以其中一条回溯路径为例:

当函数走到第5层时,cnt == 5 && 输出: 12345.然后返回到第4层调用的地方,执行vis[5] = 0 && i++;

i == 6 > 5,此时函数退出(注意退出之后去了哪里)

此时回到第3层调用的地方,执行vis[4] = 0 && i++;

i == 5 && vis[5] == 0,则vis[5] == 1 && 再次递归dfs(a,4,len)

进入递归函数第4层,此时vis[4] == 0,则vis[4] = 1 && 再次递归dfs(a,5,len)

cnt == 5,输出12354 && 返回

...

这个过程只能模拟到这里了,不可能一个一个罗列出来,大致的思路就是,通过递归和vis[]数组实现退步回溯的目的,退一步不行就退两步,一直到无路可退整个搜索就结束了.

2.迷宫求解/最短路问题

现在给你一个n*m的迷宫地图,问是否存在一条路,使得迷宫可以从起点走到终点?

对于这样的问题,如果用深度优先搜索的策略实现,我们需要搞清楚几个问题:

1.如何表示一个图?

2.如何确定搜索方向?

首先,存一个图我们使用邻接矩阵,直观易于理解(邻接表也可以但是不在这里过多介绍): 定义一个二维矩阵,e[][],e[i][j] = '.'表示坐标(i,j)的点不存在障碍,e[i][j] = '#'表示点(i,j)存在障碍.

然后我们可以使用一个二维方向数组next[][],来表示搜索的方向: next[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};分别表示搜索的4个方向,当然也可以是next[8][2] = {{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1}}来表示8个方向.

假设我们有一个这样的图:

现在确定两个点(非障碍点),一个是起点,一个是终点,请你求出起点到终点的最短路径.

思路: 首先确定我们搜索的方向--顺时针方向(通过next[][]进行转移),如果下一个点是非障碍点且没有超出地图边界,则往下一点转移,否则方向进行转移(一直沿着某个方向搜索到不能继续才转移方向),如果到达终点,则记录这条路径长度,直到所有路径尝试完以后,选择一条最短的路径,输出它的长度.代码如下:

#include<iostream>
#include<cstring>
using namespace std;

typedef struct node
{
    int x,y;
}node;
bool vis[5][5];
int nxt[4][2] = {{0,1},{1,0},{0,-1},{-1,0}},Min = 0x3f3f3f3f;
char e[5][5] = {{"..#."},{"...."},{"..#."},{".#.."},{"...#"}};

void dfs(node cur,node stp,int step)
{
    if(cur.x == stp.x && cur.y == stp.y)
    {
        Min = min(Min,step);
        return;
    }
    for(int i = 0; i < 4; i++)
    {
        node tcur;
        tcur.x = cur.x + nxt[i][0];
        tcur.y = cur.y + nxt[i][1];
        if(tcur.x < 0 || tcur.x >= 5 || tcur.y < 0 || tcur.y >= 4)
            continue;
        if(!vis[tcur.x][tcur.y] && e[tcur.x][tcur.y] == '.')
        {
            vis[tcur.x][tcur.y] = 1;
            dfs(tcur,stp,step+1);
            vis[tcur.x][tcur.y] = 0;
        }
    }
}

int main()
{
    node s,t;
    s.x = s.y = 0;
    t.x = 3,t.y = 2;
    memset(vis,0,sizeof vis);
    dfs(s,t,0);
    cout<<Min<<endl;
    return 0;
}

那么如何输出一条最短路径呢?

虽然一般不推荐用DFS找最短路径并输出,但硬要用来输出也很简单,我们仿照1的结构,用一个数组1来存当前路径,另一个数组2存最短路径,当最短路径更新时,用数组1的内容更新数组2即可,最后搜索结束后数组2存的就是一条最短路径.代码如下:

#include<iostream>
#include<cstring>
using namespace std;

typedef struct node
{
    int x,y;
}node;
node path[20],tmp[20];      //path[]用于保存最短路径;
bool vis[5][5];
int nxt[4][2] = {{0,1},{1,0},{0,-1},{-1,0}},Min = 0x3f3f3f3f;
char e[5][5] = {{"..#."},{"...."},{"..#."},{".#.."},{"...#"}};

void dfs(node cur,node stp,int step)
{
    if(cur.x == stp.x && cur.y == stp.y)
    {
        if(step < Min)
        {
            for(int i = 0; i < step; i++)    //更新最短路径;
                path[i] = tmp[i];
            Min = step;
        }
        return;
    }
    for(int i = 0; i < 4; i++)
    {
        node tcur;
        tcur.x = cur.x + nxt[i][0];
        tcur.y = cur.y + nxt[i][1];
        if(tcur.x < 0 || tcur.x >= 5 || tcur.y < 0 || tcur.y >= 4)
            continue;
        if(!vis[tcur.x][tcur.y] && e[tcur.x][tcur.y] == '.')
        {
            vis[tcur.x][tcur.y] = 1;
            tmp[step] = tcur;               //保存每一个经过的点;
            dfs(tcur,stp,step+1);
            vis[tcur.x][tcur.y] = 0;
        }
    }
}

int main()
{
    node s,t;
    s.x = s.y = 0;
    t.x = 3,t.y = 2;
    memset(vis,0,sizeof vis);
    dfs(s,t,0);
    cout<<Min<<endl;
    cout<<s.x<<" "<<s.y<<endl;
    for(int i = 0; i < Min; i++)
        cout<<path[i].x<<" "<<path[i].y<<endl;
    return 0;
}

当然深搜不止以上两个方面的应用,但更深层次的深搜,往往都不是单独使用的,比如用于优化某些算法等,这里不再详细介绍,但是深搜的思路却是不变的,即"不撞南墙不回头!".希望读者能够归纳总结,相信掌握深搜,也是一件很容易的事情.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值