一文看懂dfs深度搜索算法

dfs,即深度优先搜索算法,运用一次次递归来实现对所有结果的枚举

例题:

以下题为例:

 要从西北角走到东南角,可以横向或纵向移动,所以非常符合dfs的应用场景,接下来以本题为例分析dfs的创建思路。

思路:

本题给出靶子上箭的数目,要求骑士行走路径,我第一时间想到的就是再额外定义两个数组用于存放射出的箭的数目,初始化这两个数组的值全为0,走到哪就往北往西射一箭,北数组与西数组对应的格子里加一,当走到最后一个格子后与一开始输入的已知的靶子上箭数组进行比对,若已知北箭靶数组与北数组相等,已知西箭靶数组与西数组相等,则满足条件,输出路径。

如图:

所以此题就是先定义两个数组,然后用dfs算法进行比对。dfs算法就像一块积木,哪里需要用到一个特殊的需求,就在这块积木上增添一些东西即可。

dfs基础模块可分为三部分,分别为初始化判断条件回溯

初始化基本上就是定义一些必须要用的数组与变量,比如说要判断此路径是否走过就需要定义一个初始全是0的数组,走过即将0变为1以示走过。

判断条件是根据具体问题具体分析,有什么要求就有什么判断条件。

回溯,虽然名字看起来很难,但其实并不难,重点就是你在dfs开始时做的改变要在dfs结束时再变回去。

下面进行具体代码的分析:

初始化:

dfs一般应用与迷宫问题,并一般会让你求路径,所以我们先定义一个包含迷宫所有编号的二维数组出来

static int [][]map;
static int [][]isempty;
static int [] north;
static int [] west;
static int [] north2;
static int [] west2;

map:先定义一下二维数组map,因为设定迷宫是N*N的方阵,但N是需要输入的,我们事先并不知道要为该数组开辟多大的空间,所以在这里先不new一个空间,而是先定义一下。

isempty:这个就是判断是否走过该路径的数组,没走过为0,走过会变为1,该数组与map数组行数和列数一致,可以看成是map数组下面叠了一个数组。

north和west:这两个数组就是用于接收输入的靶子个数,之后用于比对。

north2和west2:这两个数组就是新创立的记录靶子箭数的新数组。

接下来就是具体的初始化:

N=sc.nextInt();
        if(N<0||N>20) {
            System.out.println("date is not true");
            return;
        }
        north=new int [N];
        west=new int [N];
        north2=new int [N];
        west2=new int [N];
        for (int i = 0; i <N; i++)
        {
            north[i] = sc.nextInt();
        }
        for (int i = 0; i <N ; i++) {
            west[i]=sc.nextInt();
        }
        //north= new int[]{2, 4, 3, 4};
        // west= new int[]{4,3,3,3};
        for (int i = 0; i <N; i++) {
            north2[i]=0;
        }
        for (int i = 0; i <N; i++) {
            west2[i]=0;
        }
        map=new int[N][N];
        isempty=new int[N][N];
        int count=0;
        for (int i = 0; i <N; i++) {
            for (int j = 0; j <N; j++) {
                map[i][j]=count;
                count++;
            }
        }
        for (int i = 0; i <N; i++) {
            for (int j = 0; j <N; j++) {
                isempty[i][j]=0;
            }
        }

已知行数和列数N,所以new一个[N][N]的数组,将已知靶子箭数输入到north和west数组中,

将编号输入到map中,将isempty中全初始化为0。

判断条件:

只需要在最后时刻判断原数组与新数组是否一致即可,

if(x==N||y==N){
    return;
}
if(x<0||y<0){
    return;
}
if(isempty[x][y]==1)
            return;
if(x==N-1&&y==N-1){
            if(Arrays.equals(north,north2)&&Arrays.equals(west,west2)){
                System.out.println(step);
            }
        }

首先先剔除掉不符合要求的坐标,已经超过界限的用return剔除掉。

然后判断坐标是否已走到最后一格,即(N-1,N-1)处,然后判断north数组与north2数组,west与west2数组是否相同,相同则打印step。dfs的第二个部分就完成了。

回溯:

回溯是这三个部分里最难的也是最重要的。

dfs在前面经过判断后开始进行操作,即往北往西各射一箭,即数组对应位置上++,将isempty对应的位置上置1,如何判断是否走到终点,没有走到那就接着走下一步,此时的坐标为(x,y)下一步坐标即为(x+1,y),(x,y+1),(x-1,y),(x,y-1),走过这四种可能的步数后就是所谓的回溯了,将一开始进行的操作再操作回去,将isempty设为0,将north2和west2对应位置再减回去,这就是所谓的回溯。

isempty[x][y]=1;
step=step+String.valueOf(map[x][y])+" ";
        north2[y]++;
        west2[x]++;
        if(x==N-1&&y==N-1){
            if(Arrays.equals(north,north2)&&Arrays.equals(west,west2)){
                System.out.println(step);
            }
        }
        dfs(x+1,y,step,isempty,north2,west2);
        dfs(x,y+1,step,isempty,north2,west2);
        dfs(x-1,y,step,isempty,north2,west2);
        dfs(x,y-1,step,isempty,north2,west2);
        north2[y]--;
        west2[x]--;
        isempty[x][y]=0;
合体:

完全体就是将这些合在一起

import java.util.Arrays;
import java.util.Scanner;
public class Main {
    /*输入
    4
    2 4 3 4
    4 3 3 3
    */
    //输出
    //0 4 5 1 2 3 7 11 10 9 13 14 15
    /*0 1 2 3
     * 4 5 6 7
     * 8 9 10 11
     * 12 13 14 15*/
    static Scanner sc=new Scanner(System.in);
    static int N;
    //static int [][]map={{0,1,2,3},{4,5,6,7},{8,9,10,11},{12,13,14,15}};
   // static int [][]isempty={{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}};
    static int [][]map;
    static int [][]isempty;
    static int [] north;
    static int [] west;
    static int [] north2;
    static int [] west2;
    public static void main(String[] args) {
        //N=4;
        N=sc.nextInt();
        if(N<0||N>20) {
            System.out.println("date is not true");
            return;
        }
        north=new int [N];
        west=new int [N];
        north2=new int [N];
        west2=new int [N];
        for (int i = 0; i <N; i++)
        {
            north[i] = sc.nextInt();
        }
        for (int i = 0; i <N ; i++) {
            west[i]=sc.nextInt();
        }
        //north= new int[]{2, 4, 3, 4};
        // west= new int[]{4,3,3,3};
        for (int i = 0; i <N; i++) {
            north2[i]=0;
        }
        for (int i = 0; i <N; i++) {
            west2[i]=0;
        }
        map=new int[N][N];
        isempty=new int[N][N];
        int count=0;
        for (int i = 0; i <N; i++) {
            for (int j = 0; j <N; j++) {
                map[i][j]=count;
                count++;
            }
        }
        for (int i = 0; i <N; i++) {
            for (int j = 0; j <N; j++) {
                isempty[i][j]=0;
            }
        }
        ///_init_//
        dfs(0,0,"",isempty,north2,west2);
    }
    static void dfs(int x,int y,String step,int [][] isempty,int[] north2,int[] west2){
if(x==N||y==N){
    return;
}
if(x<0||y<0){
    return;
}
        if(isempty[x][y]==1)
            return;
isempty[x][y]=1;
step=step+String.valueOf(map[x][y])+" ";
        north2[y]++;
        west2[x]++;
        if(x==N-1&&y==N-1){
            if(Arrays.equals(north,north2)&&Arrays.equals(west,west2)){
                System.out.println(step);
            }
        }
        dfs(x+1,y,step,isempty,north2,west2);
        dfs(x,y+1,step,isempty,north2,west2);
        dfs(x-1,y,step,isempty,north2,west2);
        dfs(x,y-1,step,isempty,north2,west2);
        north2[y]--;
        west2[x]--;
        isempty[x][y]=0;
    }
}

一百行代码都不到,却能完成这样强大的功能,这就是dfs的魅力。但是,要对迷宫的全部路径可能性进行枚举,这个循环量一定是十分巨大的,所以很容易就会超时。所以dfs在实际应用时就需要进行简化和剪枝。剪枝根据具体问题不同剪枝步骤也不同,所以这里只进行化简与改良。

改良:

        在这道题里,一个重大改良就是不再创造一个新数组north2来与旧数组north进行对比,而是直接在north中对应位置进行--,我称之为拔箭操作,即将靶子上的箭一根根拔下来,最终都拔成0即为满足要求。

        其次dfs每次要进行4行,且每次都需要现加减,这样很不好,于是定义两个数组,存储xy需要加的量,如:

static int[]dx={0,1,0,-1};
static int[]dy={1,0,-1,0};

这样在用的时候只需要让新的xx=x+dx,yy=y+dy即可满足对x,y的操作,再加一个for循环,这样循环4次即可涵盖上下左右四种步数。

        要记录所走路径,路径就是编号的序列,但编号要用for循环一个个加未免有些不便,于是变想能不能用x,y就能求得所对应的编号呢?果然可以,只需用(x * N) + y即可。

        定义一个int类型的变量step,用于存储所走的步数,再定义一个int类型的数组用于记录走过的编号,stepall[step] = (x * N) + y;这样就可以将编号存储在stepall这个数组中了。这样比用String类型一直进行字符串的拼接快多了。

        所以经过以上改良,将代码核心层进行更改

更改前:

isempty[x][y]=1;
step=step+String.valueOf(map[x][y])+" ";
        north2[y]++;
        west2[x]++;
        if(x==N-1&&y==N-1){
            if(Arrays.equals(north,north2)&&Arrays.equals(west,west2)){
                System.out.println(step);
            }
        }
        dfs(x+1,y,step,isempty,north2,west2);
        dfs(x,y+1,step,isempty,north2,west2);
        dfs(x-1,y,step,isempty,north2,west2);
        dfs(x,y-1,step,isempty,north2,west2);
        north2[y]--;
        west2[x]--;
        isempty[x][y]=0;

 更改后:

stepall[step] = (x * N) + y;
        north[y]--;
        west[x]--;
        if(x==N-1&&y==N-1&&check())
        {
            for (int i = 0; i <=step; i++) {
                System.out.print(stepall[i]+" ");
            }
        }
        for (int k = 0; k <4 ; k++) {
            int xx=x+dx[k];
            int yy=y+dy[k];
            if(xx>=0&&yy>=0&&xx<N&&yy<N&&isempty[xx][yy]!=1){
                if(north[yy]>0&&west[xx]>0){
                isempty[xx][yy]=1;
                dfs(xx,yy,step+1);
                isempty[xx][yy]=0;
            }
            }
        }
        north[y]++;
        west[x]++;

更改之后有一个巨大的优势就是不用再制作那么多的数组与north,west数组进行比较

且在放弃使用 string来存储路径后速度快了许多

总的版本:

import java.util.Arrays;
import java.util.Scanner;
public class Main {
    static Scanner sc=new Scanner(System.in);
    static int N;
    static int [][]isempty;
    static int [] north;
    static int [] west;
    static int [] stepall;
    static int[]dx={0,1,0,-1};
    static int[]dy={1,0,-1,0};
    static int[]zero;
    public static void main(String[] args) {
        //N=4;
        N=sc.nextInt();
        stepall=new int[N*N];
        if(N<0||N>20) {
            System.out.println("date is not true");
            return;
        }
        north=new int [N];
        west=new int [N];
        zero=new int [N];
        for (int i = 0; i <N; i++)
        {
            north[i] = sc.nextInt();
        }
        for (int i = 0; i <N ; i++) {
            west[i]=sc.nextInt();
        }
        for (int i = 0; i <N ; i++) {
            zero[i]=0;
        }
        //north= new int[]{2, 4, 3, 4};
        // west= new int[]{4,3,3,3};
        isempty=new int[N][N];
        for (int i = 0; i <N; i++) {
            for (int j = 0; j <N; j++) {
                isempty[i][j]=0;
            }
        }
        ///_init_//
        dfs(0,0,0);
    }
    static void dfs(int x,int y,int step){
        isempty[x][y]=1;
        stepall[step] = (x * N) + y;
        //stepall[step]=map[x][y];
        north[y]--;
        west[x]--;
        if(x==N-1&&y==N-1&&check())
        {
            for (int i = 0; i <=step; i++) {
                System.out.print(stepall[i]+" ");
            }
        }
        for (int k = 0; k <4 ; k++) {
            int xx=x+dx[k];
            int yy=y+dy[k];
            if(xx>=0&&yy>=0&&xx<N&&yy<N&&isempty[xx][yy]!=1){
                if(north[yy]>0&&west[xx]>0){
                dfs(xx,yy,step+1);
                isempty[xx][yy]=0;
            }
            }
        }
        north[y]++;
        west[x]++;
    }
    static boolean check(){
       if(Arrays.equals(zero,north)&&Arrays.equals(zero,west))
           return true;
           else
               return false;
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值