HDOJ.1010 Tempter of the Bone (DFS)

Tempter of the Bone [从零开始DFS(1)]

从零开始DFS
HDOJ.1342 Lotto [从零开始DFS(0)] — DFS思想与框架/双重DFS
HDOJ.1010 Tempter of the Bone [从零开始DFS(1)] —DFS四向搜索/奇偶剪枝
HDOJ(HDU).1015 Safecracker [从零开始DFS(2)] —DFS四向搜索变种
HDOJ(HDU).1016 Prime Ring Problem (DFS) [从零开始DFS(3)] —小结:做DFS题目的关注点
HDOJ(HDU).1035 Robot Motion [从零开始DFS(4)]—DFS题目练习
HDOJ(HDU).1241 Oil Deposits(DFS) [从零开始DFS(5)] —DFS八向搜索/双重for循环遍历
HDOJ(HDU).1258 Sum It Up (DFS) [从零开始DFS(6)] —DFS双重搜索/去重技巧
HDOJ(HDU).1045 Fire Net [从零开始DFS(7)]—DFS练习/check函数的思想

题意分析

给出一张大小为n * m的地图,其中:
X代表墙壁,走不通;
S代表起点;
D代表终点;
. 代表空白区域,即可以走通。
求解是否正好可以在第T步的时候走到终点,并且不能走回头路(即走过的地方不能再走)。

典型的地图搜索问题嘛,首先想到的就是DFS。先回顾一下上次探讨的内容。
传送门:

HDOJ.1342 Lotto [从零开始DFS(0)]

1.几个名词
DFS的搜索原则
递归
剪枝
递归边界

2.DFS的函数模型

void dfs( 参数 )
{
    // 递归边界
    // 可以是检查是否满足解的要求

    // 完成某系列动作
    // 继续递归调用dfs
}

3.其实上次有2个问题没有讨论的太明白

(1)
Q:没有进行排序,为什么自动是字典序?

A:因为题目给的数据,即数组a本来就是按照升序排好的,按照上次说的搜索原则,搜索到的解,一定是按照递增排列好的。每次递归调用dfs的时候,都是先假定选取这个数字,然后再看不选这个数字的情况,那么选取的这个数字,肯定比不选这个数字以后的情况来的小,所以他的下一个解,一定比这个解的字典序要大。如:

位置 1 2 ……
a数组 1 2 ……
b数组 1 待定 ……

若现在b的第二个位置选了2

位置 1 2 ……
a数组 1 2 ……
b数组 1 2 ……

那么这组解就是12XXXX

若不选2呢?

位置 1 2 ……
a数组 1 2 ……
b数组 1 待定(3、4等都有可能) ……

那么这组解就有可能是:
13XXXX 或者 14XXXX 自然字典序就比上面的情况来的大。

(2)
Q: 在递归调用的时候,如何判断这个节点(或者数组的某个元素)已经访问过了呢?

A:因为上次的题是一维数组并且按顺序访问的,就不用检查是否访问过了。但是这个题就需要检查了,一般采用的是visit数组。

先上代码,掰开了揉碎了慢慢来。。

代码总览

/*
    Title:HDOJ.1010
    Author:pengwill
    Date:2017-2-4
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m,t,x1,y1,x2,y2;
int spx[] = {1,0,-1,0};
int spy[] = {0,1,0,-1};
char mp[10][10];
bool visit[10][10];
bool judge = false;
void init()
{
    for(int i = 0;i<n;++i){
        for(int j = 0;j<m ;++j){
            if(mp[i][j] == 'S') {x1 = i;y1 = j;}
            else if(mp[i][j] == 'D') {x2 = i;y2 = j;}
            else if(mp[i][j] == 'X') visit[i][j] = true;
        }
    }
}
bool check(int x, int y)
{
    if(x<0||x>=n||y<0||y>=m) return false;
    else return true;
}
void dfs(int x, int y, int s)
{
    if(check(x, y) == false)return;
    visit[x][y] = true;
    if(x==x2 && y == y2 && s == t){
        judge = true;
        return;
    }
    //int temp = t-((abs(x1-x2) + abs(y1-y2)));
    //if(temp<0 ||temp %2 ==1) return;
    if(((t-s)%2 != (abs(x-x2) + abs(y-y2)) %2)) return;
    for(int i = 0;i<4;++i){
        if(!visit[x+spx[i]][y+spy[i]]&&!judge){
            dfs(x+spx[i],y+spy[i],s+1);
            visit[x+spx[i]][y+spy[i]] = false;
        }
    }

}
int main()
{
  //  freopen("in.txt","r",stdin);
    while(scanf("%d%d%d",&n,&m,&t) &&(n+m+t)){
        memset(mp,0,sizeof(mp));
        memset(visit,0,sizeof(visit));
        for(int i = 0;i<n;++i)
            scanf("%s",mp[i]);
        init();
        judge = false;
        dfs(x1,y1,0);
        if(judge) printf("YES\n");
        else printf("NO\n");
    }
    return 0;
}

首先是main函数

    while(scanf("%d%d%d",&n,&m,&t) &&(n+m+t)){
        memset(mp,0,sizeof(mp));
        memset(visit,0,sizeof(visit));
        for(int i = 0;i<n;++i)
            scanf("%s",mp[i]);
        init();
        judge = false;
        dfs(x1,y1,0);
        if(judge) printf("YES\n");
        else printf("NO\n");
    }

按照题意读取nmt三个数据,并且初始化地图数组mp检查是否访问过的数组visist,之后读入地图的数据,接着是init函数,看看init做了什么。

void init()
{
    for(int i = 0;i<n;++i){
        for(int j = 0;j<m ;++j){
            if(mp[i][j] == 'S') {x1 = i;y1 = j;}
            else if(mp[i][j] == 'D') {x2 = i;y2 = j;}
            else if(mp[i][j] == 'X') visit[i][j] = true;
        }
    }
}

原来是找到起点S和终点D的位置,并且把墙所在坐标的visit写为true,代表访问过。不难理解,既然走不通,就把它变成访问过,只要在dfs过程中稍加判断也不会再次访问了。

回到main函数,接着是吧judge改为false,这个是递归边界。题目只要求找出能还是不能,如果我搜索到某一情况,判定可以走到终点,那么明显就没有必要继续搜索下去了。接下来是重头戏,dfs函数。

void dfs(int x, int y, int s)
{
    if(check(x, y) == false)return;//判断是否出界
    visit[x][y] = true;//改变当前节点为访问过
    if(x==x2 && y == y2 && s == t){//判断是否满足题目要求的解
        judge = true;
        return;
    }
    if(((t-s)%2 != (abs(x-x2) + abs(y-y2)) %2)) return;//奇偶剪枝
    if(t-s-abs(x-x2)-abs(y-y2)<0) return;//步数不够剪枝
    for(int i = 0;i<4;++i){//递归调用dfs
        if(!visit[x+spx[i]][y+spy[i]]&&!judge){
            dfs(x+spx[i],y+spy[i],s+1);
            visit[x+spx[i]][y+spy[i]] = false;
        }
    }

}

首先dfs有3个形式参数,xy代表入口坐标,即搜索入口,s代表走的步数。首先按照check函数,检查当前坐标是否超出地图边界,若超出,则终止递归。

check函数:

bool check(int x, int y)
{
    if(x<0||x>=n||y<0||y>=m) return false;
    else return true;
}

接着把当前的位置,变为访问过。紧接着就是判断当前坐标是否就是终点坐标并且看步数是否可题目要求的一致,若是的话,judge变为true表示找到解了,以后递归都没必要做了。
然后是奇偶剪枝

这里写图片描述
(图2-1)

Tip:只能上下左右走,不能斜着走。

如图2-1所示,由start到end图中最短路径为红色,走了12步。相比于红色路径,蓝色路径大费周折,大家可以数一下,共走了14步。此时步数的偏移量为14-12=2。奇偶剪枝,就是步数的偏移量永远为偶数(可证明),因此可以得出:最短路径步数+某一偶数(即偏移量)=某一可行解歩数。(下面这句是重点)即最短路歩数和某一可行解歩数的奇偶性相同

回到题目,若已知最短路径歩数为偶数,且在某一情况下剩余的步数为奇数,那么无论如何也是无法恰好到达终点的,因此这种情况就不用算啦,终止递归。当然步数不够,也是走不到终点的。

接着递归调用dfs,这里有新东西啦,visit[x+spx[i]][y+spy[i]]是什么鬼。

看一下开头定义的的:

int spx[] = {1,0,-1,0};
int spy[] = {0,1,0,-1};

不难发现,spx为0时spy为±1,反之成立。也可以想到,x±1和y±1分别对应的就是地图上,上下左右移动一个单位。这里是个小trick,用这样一个的数组来实现上下左右的移动,毕竟一个for循环多好写。
当且仅当下一个位置没有访问过,并且这个时候还没有找到可行解时,递归调用dfs,在调用结束后,记得要把visit[x+spx[i]][y+spy[i]]置为false。原因很简单,不能影响到下一次循环呀:对于下一次循环[x+spx[i]][y+spy[i]]这个位置,可是没有访问过的。(无后效性??请各位指点)。 这样一道题,就ac了。

多做题,加紧训练吧!

posted @ 2017-02-04 15:49  pengwill  阅读(223)  评论(0编辑  收藏  举报