算法(三)搜索算法(一)(深度优先搜索)之地图迷宫

前言

前面两篇文章,只是讲了一下算法的入门,排序算法和枚举算法,大家是不是觉得很简单呢?那么这篇文章,我们就有学习一下,稍微有点难度的,也是面试、笔试经常见到的深度优先搜索。而常见的搜索算法就有,上篇文章中所介绍的穷举算法、以及本篇将要介绍的深度优先搜索,以及暂时不会介绍的A*算法、回溯算法、混沌搜索等,当然最高深的搜索算法,就是百度和google的看家本领了。那就不是我们现在能够理解的范围了,他们是江湖上成名已久的大侠如乔峰、郭靖,而我们还是出入江湖的毛头小子,所以先练好我们的基础吧。

深度优先搜索

让我们先来学习一下深度优先搜索算法,所谓的深度优先搜索就是关键在于解决“当下该如何做”,至于“下一步该如何做”与“当前该如何做”是一样的。例如爬虫程序时候,我们在爬某个节点的时候,需要爬这个节点下方所有的节点,爬完之后,再返回到这个节点的同级节点,并从这个同级节点开始往下继续爬,依次最后达到爬完所有节点的目的。

深度优先搜索的基本模型
 public void depthFirstSearch(int step){
        1、判断边界
        2、尝试每一种可能
        for (int i = 0; i <= n; i++) {
            3、继续下一步 
            depthFirstSearch(step + 1);
        }
        return;
    }

实际需求
上面这样讲,肯定还是有点抽象,下面我们用一个例子来说明。比如,任意输入一个2到9之间的数字,我们打印出所有存在的排列组合。
例:输入  2   输出 12  21
        输入  3  输出  123  132  213  231  312   321

思路分析
首先,我们把这个问题具体化,比如 面前有 n个盒子,我们手上有序号为1 - n 的n个小球,然后我们需要依次将手里的小球,放入盒子中 ,那
么每次我们排列完成(手上没有小球)的时候 ,盒子中的小球序列就是我们要输出的序列
然后,我们分别来定义几个东西,首先 数组a[] 表示盒子序列,  数组ball[] 表示,我们手上有的小球,range表示一共有多少个盒子。
 public void printRank(int n){
        if (n < 2 || n > 9 ){
            return;
        }
        a = new int[n+1];//为了好理解,我们都是从1号开始的,所以多初始化一个
        ball = new int[n+1];
        range = n;
    }
定义好盒子和小球之后我们需要思考,我们接下来该怎么做呢,其实我们的核心操作就是把小球分别放进盒子里,这个放置规则,我们
需要自己定一下,比如基本的放置规则是从1到n,意思是我们思考第step个盒子如何放置的时候,先从1开始,如果1已经放置了 就考虑2,这
样依 次来。
所以,其实核心就是一个for循环,那么如何判断小球已经被我们放进盒子里了呢,就让第index号表示小球的位置ball[index] == 1;//数组
初始化的时候,会默认为0。
so,我们可以得到以下的核心代码,第step个盒子的小球存放方式
   public void dfs(int step){
        for (int i = 1; i < range; i++) {
            //遍历小球
            if (ball[i] == 0){
                //表示,小球还在我们手上,那么我们讲当前小球,放进当前这一步的盒子里
                a[step] = i; 
                ball[i] = 1;//当前这个小球,已经放进第step个盒子里面去了
                ...              
            }
        }
    }

那么下一个盒子如何放小球呢??其实跟第step个盒子的放置逻辑是相同的,也就是说我们可以通过递归的逻辑,来重复调用dfs()方法。
public void dfs(int step){
        for (int i = 1; i < range; i++) {
            //遍历小球
            if (ball[i] == 0){
                //表示,小球还在我们手上,那么我们讲当前小球,放进当前这一步的盒子里
                a[step] = i; 
                ball[i] = 1;//当前这个小球,已经放进第step个盒子里面去了
                dfs[step + 1];    
                ball[i] = 0;//非常重要,我们要回收i号小球,才能进行下一次尝试     
            }
        }
    }
核心的放置规则,我们已经清楚了,但是什么时候结束依次排列呢?也就是说现在我们需要判断边界了,那么什么是边界呢?也就是,如
果我们已经在放置第range+1个盒子了,那么前面的所有已经是放置完毕了的。
   if (step == range + 1){
            //说明前面range个盒子已经放置完毕了d
            StringBuilder result = new StringBuilder();
            for (int i = 1; i < range; i++) {
                if (i == 1){
                    result.append(a[i]);
                }else {
                    result.append(",").append(a[i]);
                }
            }
            Log.i("hero","----"+result.toString());
            return;
       }

到了这一步,这个算法的核心代码我们已经编写完毕了,整个方法代码如下
public void dfs(int step){
        if (step == range + 1){
            //说明前面range个盒子已经放置完毕了d
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < range; i++) {
                if (i == 0){
                    result.append(a[i]);
                }else {
                    result.append(",").append(a[i]);
                }
            }
            Log.i("hero","----"+result.toString());
            return;
        }
        for (int i = 1; i <= range; i++) {
            //遍历小球
            if (ball[i] == 0){
                //表示,小球还在我们手上,那么我们讲当前小球,放进当前这一步的盒子里
                a[step] = i;
                ball[i] = 1;//当前小球已经没有了
                dfs(step + 1);//进行第step个盒子放置小球的判定
                ball[i] = 0;//这行代码非常重要,该代码前面,尝试了将i号球,放入第step号盒子中,想要继续进行下面的尝试,必须把第i号球回收
            }
        }
        return;
    }

下面,我们编写初始化代码
 public void printRank(int n){
        if (n < 2 || n > 9 ){
            return;
        }
        a = new int[n + 1];
        ball = new int[n + 1];
        range = n;
        dfs(1);//从第一个盒子开始判断
    }

下面让我们输入一个2,然后根据代码来看思路是否正确
	print(2);

代码执行过程具体分析
(警告!!!以下过程略显枯燥,请注意调节自我心情,避免出现不适反应。)
A、初始化,a[]、ball[]、range = 2,开始放置第1个盒子

B、放置第一个盒子,dfs[1],判断边界,不符合,遍历小球1、2, 判断小球1是否在手上,ball[1],在手上,那么把小球1放进,盒子1,小球
不在手上了,开始排列决定第二个盒子放什么

C、放置第二个盒子,dfs[2],判断边界,不符合,遍历小球1、2,判断小球1是否在手上,ball[1],不在手上,继续判断小球2是否在手上,
ball[2],在手上,把小球2,放进盒子2,标记小球2不在手上,开始决定第三个盒子放什么,

D、放置第三个盒子,dfs[3],判断边界,符合(我们一共只有两个盒子,当他排列第三个盒子的时候,说明1、2号盒子已经放置好了,),
遍历打印,输出, [1,2] ,return

E、这个时候代码会return到步骤C的dfs[3]方法后面的代码,执行ball[2],将第二盒子里面的小球2拿出来,C步骤的for循环,已经执行完毕,
return,这时候,小球2在手上,盒子2是空的
F、代码返回到步骤B的for循环,之前我们只判断了小球1是否在手上,也就是执行了dfs[2]方法,现在执行dfs[2],后面的代码,ball[1] = 0,
也就是将盒子1里面的小球1拿在手上,然后步骤B的for循环,开始走到i == 2,这时小球1、2都在我们手上

G、判断小球2是否在手上,ball[2],在手上,将小球2,放进盒子1,开始决定排列第二个盒子放什么 ,dfs[2]

H、放置第二个盒子,dfs[2],判断边界,不符合,遍历1、2小球,判断小球1是否在手上,ball[1],在手上,将小球1放进盒子2,开始决定第
三个盒子放什么,dfs[3]

I、放置第三个盒子,dfs[3],判断边界,符合,打印结果,[2,1],return

J、dfs[3],执行完毕了,现在返回到步骤H,把小球1,拿回来,之前的遍历只走到了判断小球1,现在开始判断小球2,小球2不在手上,返回
到,步骤F,的for循环

K、步骤F的for循环走完了,拿回小球2,return,这时候,小球1、2都在我们手上,执行完了for循环,返回到步骤B的for循环,也就是递归的
第一层for循环,这时候步骤B的for循环已经走完了,return

L、递归的第一层,走完毕,函数return掉

代码的执行结果
很明显,预期和我们的分析是一样的,我们已经打印出了n == 2时,的所有排列组合,12,21。
<Image_1>
<Image_2>

地图迷宫

上面,我们学习了深度优先搜索算法,下面,我们来用深度优先搜索算法,解决一个小问题,哈哈哈哈,这就是大名鼎鼎的地图迷宫。
题目是这样的,你的女朋友一个人去走迷宫,但是她迷失在迷宫里面了,现在需要你去找到她,当然,现在的你已经拿到了地图了,如果是我们人类的话,我们可以根据地图,轻松的找到一条最短的路径去救回你的女朋友,那么,如何使用程序来实现这个需求呢?现在我们就利用深度优先搜索算法来实现这样一个能够自动计算最短路线的程序。

具体问题
现在我们首先将地图坐标系化,也就是用x、y坐标点来表示地图中的某一个坐标点,整张地图其实就是一个x、y轴的坐标系,当然地图里
面肯定有些坐标点是河流呀,山谷呀等等,我们不能去的地方,而且我们不能走出地图范围,首先让我们来简单画一张我们需要的地图。
<Image_3>
我们已经拿到地图了,你的GF所在位置为(3、4),你所在的位置为(1,1),网状点就是河流、山峰等不可经过的地方,现在你需要
用一条最短的路线,找到你女朋友了。

思路分析
我们已经决定用深度优先搜索来完成,那么深度优先的特点是什么呢?他关注当前这个节点,我们做什么,下个节点的做法和当前节点做
法完全一致,那么这个程序的核心问题是什么呢?就是当我们在某一个点的时候,我们有上、下、左、右四个方向可以去,那么我们应该去哪
个方向呢?这个我们是不知道的,所以我们需要把每个方向都尝试一下,直到这个方向或者说路线出了地图或者无法继续前行了,我们就返回
上个坐标点,继续其他的点的尝试。
看到这里,你是不是发现其实核心思路和我们上面的排列问题是一模一样的?

编程过程
那么,让我们开始编写程序把。
首先思考最核心的可重复的,某个点怎么走的函数,其实这个函数,只需要维护三个参数,当前这个点的坐标x、y以及当前是第几步,
然后我们需要一个决定上、下、左、右执行顺序的数组,我们决定用顺时针的方向来表示行走的先后顺序,从上方开始aaa[][] = {{0,-1},{1,0},
{0,1},{-1,0}},该二维顺组就可以决定我们的每个点的行走顺序。

我们先来初始化地图,以及一些需要初始化的参数
    //地图迷宫
    private int[][] aaa = {{0,-1},{1,0},{0,1},{-1,0}};//每步的搜索顺序,上、右、下、左
    private int[][] map = new int[5][6];//初始化地图,其他地方为了方便是从1开始的,那么地图就多一个便于计算
    private int[][] path = new int[5][6];//已经走过的路径点
    private int q = 3;//女朋友所在的坐标点
    private int p = 4;
    private int min = 0;
    private int count = 0;
    public void mapMaze(){
        map[3][1] = 1;//初始化地图的障碍物
        map[3][3] = 1;
        map[2][4] = 1;
        map[4][5] = 1;
        min = 4 * 5;//假设要全部点都走一遍,那么一共是20步,预设一个理论最大值
        depthFS(1,1,0);//开始走
        Log.i("hero","---找到女朋友啦,最短路线为--"+min+"步");
    }

首先我们定义一个可重复的函数depthFS(),判断该步做什么,已经怎么做
public void depthFS(int x,int y,int step){
     int nx,ny;//下一步的x、y坐标
     for (int i = 0; i < aaa.length; i++) {
        nx = x + aaa[i][0];
        ny = y + aaa[i][1];
            
        depthFS(nx,ny,step+1);         
     }
}

核心代码如上,怎么样,是不是觉得,超级简单,but,我们还有一些关键的事情没处理,比如判断下一个点是否越界或者是否是障碍物
,那我们加上部分代码。
       for (int i = 0; i < aaa.length; i++) {
            nx = x + aaa[i][0];
            ny = y + aaa[i][1];
            if (nx < 1 || nx > 4 || ny < 1 || ny > 5 ){
                //说明越界了
                continue;
            }
            if ( map[nx][ny] == 0){
                //判断是否已经走过了以及该点是否有障碍物
                depthFS(nx,ny,step+1);
            }
        }
        return;

上述,代码我们加上 判断下一个点是否越界,以及是否是障碍物,但是好像我们还没添加判断边界,那么这道题的判断边界在哪里呢?是 不是 就是找到女朋友的时候呀?
 if (x == q && y == p){
       //说明找到女朋友啦
       count++;//用来表示,已经有几条线路了
       Log.i("hero","第"+count+"条路线找到女朋友,步数:"+step);
       if (step < min){
            min = step;//更新步数,如果这条线路的step比记录的少,那么更新它
       }
       return;//返回上一个节点,继续从其他方向来搜索
  }

看起来好像没问题了,but,我们程序如何判断一个点我们已经走过了呢???如果不加上这部分判断,会不会出现反复在几个点走的情
况?所以我们得加上一个用于记录已经走过路线的数组path。
for (int i = 0; i < aaa.length; i++) {
            nx = x + aaa[i][0];
            ny = y + aaa[i][1];
            if (nx < 1 || nx > 4 || ny < 1 || ny > 5 ){
                //说明越界了
                continue;
            }
            if (path[nx][ny] == 0 && map[nx][ny] == 0){
                //判断是否已经走过了以及该点是否有障碍物
                path[nx][ny] = 1;
                depthFS(nx,ny,step+1);
                path[nx][ny] = 0;//清除该点的痕迹,因为会走其他的线路
            }
        }
我们通过path来记录已经走过的点,这样我们能保证下一步往四方走的时候,不会走回来,而只会走其他的地方。

完整代码如下
    //地图迷宫
    private int[][] aaa = {{0,-1},{1,0},{0,1},{-1,0}};//每步的搜索顺序
    private int[][] map = new int[5][6];//初始化地图,其他地方为了方便是从1开始的,那么地图就多一个便于计算
    private int[][] path = new int[5][6];//已经走过的路径点
    private int q = 3;
    private int p = 4;
    private int min = 0;
    private int count = 0;
    public void mapMaze(){
        map[3][1] = 1;//初始化地图的障碍物
        map[3][3] = 1;
        map[2][4] = 1;
        map[4][5] = 1;
        min = 4 * 5;//假设要全部点都走一遍,那么一共是20步
        depthFS(1,1,0);
        Log.i("hero","---找到女朋友啦,最短路线为--"+min+"步");
    }
    /**
     * 核心可重复使用代码
     * x 当前点的x坐标
     * y 当前点的y坐标
     * step 当前是第几步
     * */
    public void depthFS(int x,int y,int step){
        int nx,ny;
        //判断边界
        if (x == q && y == p){
            //说明找到女朋友啦
            count++;
            Log.i("hero","第"+count+"条路线找到女朋友,步数:"+step);
            if (step < min){
                min = step;//更新步数
            }
            return;//返回上一个节点,继续从其他方向来搜索
        }
        for (int i = 0; i < aaa.length; i++) {
            nx = x + aaa[i][0];
            ny = y + aaa[i][1];
            if (nx < 1 || nx > 4 || ny < 1 || ny > 5 ){
                //说明越界了
                continue;
            }
            if (path[nx][ny] == 0 && map[nx][ny] == 0){
                //判断是否已经走过了以及该点是否有障碍物
                path[nx][ny] = 1;
                depthFS(nx,ny,step+1);
                path[nx][ny] = 0;//清除该点的痕迹,因为会走其他的线路
            }
        }
        return;
    }

代码执行结果如下
<Image_4>

哇,我们成功找到女朋友了,最短的路线是7步,共计有13种路线可以找到女朋友,7步的情况也有三条路线,大家想想是否是这样的呢???

总结

本篇文章,从一个数字排列的问题入手,深入的讲解了一下深度优先算法,然后通过一个地图迷宫的趣味算法题,带大家再次复习了一下深度优先搜索。是不是觉得结合有趣的问题,算法学习就不那么枯燥了呢?现在回想一下上篇文章我们学习过的穷举算法,深度优先搜索是不是就运用了穷举的思想呢?他尽可能的列出了所有的可能性,然后判断符合条件的,就是我们所需要的。深度优先算法,是图算法的一种,在早期的爬虫开发中被广泛运用。
因为篇幅有限,我们就暂时先学习到这里,预告,下一篇我们将要学习的是,深度优先搜索的好姐妹,广度优先搜索。
因个人水平有限,难免和不足之处,请多多指正。


  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值