欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基础算法·深度优先搜索

程序员文章站 2022-05-23 18:30:24
...

祝食用愉快XD

题目链接

(是一道胡乱出的题)
U56815 来走迷宫鸭!

解题思路

深度优先搜索,如果能不碰墙地到达右下角的出口,就把旗子立起来表示找到了出口。
什么?你没听过深度优先搜索
没事,且听我道来。

什么是搜索?如何搜索?

简单来说,搜索就是一种特殊的(递归的)枚举。从一种可行的方案进行扩展,并去看这个扩展出来的东西符不符合现有规则、能不能继续扩展
可是你讲理论我也听不懂啊

那,深度优先搜索又是什么呢?

拿走迷宫这事儿说起。如果你玩过\(MC\),或者无论从哪个去掉了解走迷宫的时候用的“右手法则”,那么你或许能更容易理解深度优先搜索。
如果你不了解这个法则,没有关系。还记得希腊神话里面的国王米诺斯吧。

他十分恶毒,造了一个错综复杂的宫殿,任何外人进去,都会迷失路径,别想再走出来,因此被称做迷宫。米诺斯还在迷宫深处,豢养了一只牛首人身的怪物米诺陶洛斯。他用暴力迫使雅典城的国王爱琴,每九年贡纳七对童男童女,作为牛怪的食物。有一年,雅典城又该贡献童男童女了,人们在悲痛的气氛中,准备用一艘挂着黑帆的船,把七对童男童女运送到克里特岛去,雅典少年王子特修斯出于义愤,主动要求把自己作为童男之一送去,以便杀死牛怪,为民除害。特修斯来到了克里特岛,米诺斯的女儿阿里阿德涅爱上了这位勇敢的王子,她偷偷送给他一柄魔剑和一个线团。特修斯按照她的叮嘱,在被送进迷宫之后,立即暗中把线的一端拴在宫门口,随走随放,一直进入迷宫深处。经过一番拼死搏斗,特修斯终于用魔剑刺死了牛怪。

这位勇敢的王子是怎么走进迷宫的呢?原来,他是通过线来标记自己来时的路,并及时标记已经走过的路径,避免了被迷宫绕晕、重复走相同道路的可能。
把这玩意搁程设这儿,就叫深度优先搜索(Deep First Search,DFS)。不可思议吧?
(似乎有点偏题了)好,我们回去看放在开头的那道题。
刚才说了,搜索是一种递归的枚举。怎么递归呢?这道题我们可以通过向函数里面传坐标达到搜索的目的。
先上代码,一点点解释。

#include<stdio.h>
int flag,n,m;
int state[521][521],vis[521][521];
void dfs(int x,int y);
int main(){
    int i,j;
    scanf("%d%d",&n,&m);
    for(i=1;i<=m;i++)
        for(j=1;j<=n;j++)
            scanf("%d",&state[i][j]);//state表示状态
    dfs(1,1);//初始状态是左上角,坐标是(1,1)
    printf("%s",flag?"Yes":"No");
    return 0;
}
void dfs(int x,int y){
    if(x==m&&y==n){//(0)
        flag=1;
        return;
    }
    if(x<=0||x>m||y<=0||y>n||state[x][y])return;//(1)
    if(vis[x][y])return;//(2)
    vis[x][y]=1;//(3)
    dfs(x+1,y);//(4)
    dfs(x,y+1);
    dfs(x-1,y);
    dfs(x,y-1);
}

这里用到两个数组,\(state\)数组和\(vis\)数组,其中\(state\)数组即是输入的地图,而\(vis[i][j]\)数组代表这个点被没被访问过。

来解释一下代码中的引用

\((0)\)找到出口了!很好我可以退出了(x
\((1)\)如果搜索的时候搜到地图外面去了或者这个点有个大石头墩子那肯定不行啊(x
\((2)\)如果访问过了那也肯定不行啊(
\((3)\)既然行那就插个旗子表示这个点访问过了emm
\((4)\)上下左右胡乱搜一通

开始划重点

\(0.\)这是一个递归的程序,所以一定有返回的时候,不能一直一层层的向下递归。(这句话你都看不懂的话还是去回去复习复习递归吧)
\(1.\)那么什么时候返回呢?如果这个地方满足了搜索的初衷(找到该找的点了),那自然要返回。如果这个地方不能搜下去,自然需要返回。如果这个地方被搜过,也就是说再搜一遍这个点在这个题里是没有意义的(注意:有特例),所以返回。
\(2.\)注意到,上面的三种情况分别对应程序中的\((0)(1)(2)\)
\(3.\)好了其实上面三条说的都是返回的情况。那么下面来说DFS函数里面除了返回能干啥吧。
\(4.\)这个题里面似乎并不能干啥
\(5.\)我就是来凑够五条的
把这个题稍微改一下,把出口改成所有右边界上的点,咋办呢?请思考
那么这里与原题唯一的不同就是返回的第一种情况。
给出DFS函数的参考代码

void dfs(int x,int y){
    if(y==n){
        flag=1;
        return;
    }
    if(x<=0||x>m||y<=0||y>n||state[x][y])return;
    if(vis[x][y])return;
    vis[x][y]=1;
    dfs(x+1,y);
    dfs(x,y+1);
    dfs(x-1,y);
    dfs(x,y-1);
}

喂喂你这不是没改什么东西么

好啦,不开玩笑了,下面来介绍DFS函数内部能干什么。

先来看一道题(怎么又有题)<P1605 迷宫>

这题和刚才的差不多,只是保证肯定有解,要计算路的个数。这时候咋办啊?
看到题别慌。先看啥时候退出递归
根据刚刚的五大定律,首先,如果找到出口了,那么答案++,返回;其次,如果搜到地图外面了或者不能走了或者访问过了,那么返回;再次,如果这条路上已经走过了这个点,(不能越搜越倒怵啊),那么我们也要返回。

注意这里的描述。

这次返回不是只要搜过就返回,而是在正在搜索的这条路上搜过才返回。这如何实现呢?
略微思考一下就可以明白,这其实只是加入了一个可以撤销已访问标签的操作而已。也就是说,每次在换不同路径的时候,都要把刚刚那条路径当做没走过一样,因为必然不会再走一遍那一条路,但是新的答案可能会经过那一个节点。这块很绕,可能需要思考几分钟。
贴代码:

#include<stdio.h>
int ans,n,m,t;
int state[521][521],vis[521][521];
void dfs(int,int,int,int);
int main(){
    int i,sx,sy,tx,ty;
    scanf("%d%d%d",&n,&m,&t);
    scanf("%d%d%d%d",&sx,&sy,&tx,&ty); 
    for(i=0;i<t;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        state[x][y]=1;
    }
    dfs(sx,sy,tx,ty);
    printf("%d",ans);
    return 0;
}
void dfs(int x,int y,int tx,int ty){
    if(x<=0||x>m||y<=0||y>n)return;
    if(vis[x][y]||state[x][y])return;//(1)
    if(x==tx&&y==ty){
        ans++;
        return;
    }
    vis[x][y]=1;
    dfs(x+1,y,tx,ty);
    dfs(x,y+1,tx,ty);
    dfs(x-1,y,tx,ty);
    dfs(x,y-1,tx,ty);
    vis[x][y]=0;//去除标记
}

这份代码有一个值得思考的地方:\((1)\)

为什么刚才的代码可以不把这两个判断提前,而这个程序必须把这两句话提前?

给一组数据希望能够有助理解:

3 3 2
1 1 3 3
2 2
3 3

正解输出:

0

错误输出:

2

如果不明白,请自行debug找寻其中的奥秘

现在再用DFS的方法解填充数字的题。

BHOJ T110 亚顿的幻方
这其实已经有点偏离DFS了,只是纯粹的递归。但无所谓。

#include<stdio.h>
int i,j,n,a[1001][1001];
void search(int x,int y,int num){
    if(num>n*n)return;
    a[x][y]=num;
    num++;
    if(!x&&y<n-1)search(n-1,y+1,num);
    else if(y==n-1&&x)search(x-1,0,num);
    else if(y==n-1&&!x)search(x+1,y,num);
    else{
        if(!a[x-1][y+1])search(x-1,y+1,num);
        else search(x+1,y,num);
    }
}
int main(){
    scanf("%d",&n);
    search(0,n/2,1); 
    for(i=0;i<n;i++){
        for(j=0;j<n;j++){
            printf("%d ",a[i][j]);
        }
        printf("\n");
    }
    return 0;
}

其实这种DFS完全可以转化成循环。
其实递归转化为循环是最好的。
排列、组合等等,都可以通过DFS得出,不再赘述。

好了,现在是八皇后的时间

来介绍一道经典题,它的名字叫做八皇后。

P1219 八皇后

是很好的递归题,对初学者来说思维量较大,当时花了一下午解这道题。考期将至,请合理分配自己的时间,若有兴趣可以了解一下这道题。

#include<stdio.h>
int line[20],diagonal[20],diagonal2[40],ans[20];//line,diagonal,diagonal2三个数组起到了类似vis数组的作用,表示这一列/这个对角线/另外一条对角线上有皇后,不可再放
int sum=0;
void put(int x,int y){
    line[y]=1;
    diagonal[x+y]=1;
    diagonal2[x-y+26]=1;
}
void forget(int x,int y){
    line[y]=0;
    diagonal[x+y]=0;
    diagonal2[x-y+26]=0;
}
int judge(int x,int y){
    if(!line[y]&&!diagonal[x+y]&&!diagonal2[x-y+26])
        return 1;
    return 0;
}
void search(int begin,int n){//begin:搜到第几列了
    int i,j;
    for(i=1;i<=n;i++){
        if(judge(begin,i)){//如果这里能放皇后
            ans[begin]=i;
            put(begin,i);//相当于刚刚那个题的vis[x][y]=1
            if(begin<n)search(begin+1,n);
            else{
                sum++;
                if(sum<=3){
                    for(j=1;j<=n;j++){
                        printf("%d ",ans[j]);
                    }
                    printf("\n");
                }
            }
            forget(begin,i);//相当于刚刚那个题的vis[x][y]=0
        }
    }
}
int main(){
    int n;
    scanf("%d",&n);
    search(1,n);
    printf("%d",sum);
    return 0;
}

诶诶诶!刚刚说好的必须有返回呢!!
emmm这难道是打脸
不是啦!\(return\)的意义其实在于防止递归一直一直一层一层在进行,导致爆栈。而这个函数\(for\)里面的\(if\)语句有不能执行进去进行下一层递归的时候,这时候其实已经相当于\(return\)啦!

祝期末考试RP++!!!