DFS和BFS的一点简单总结

来源:互联网 发布:android java 编辑:程序博客网 时间:2024/06/05 15:00

       DFSBFS的一点简单总结

 

 搜索,简单狭窄地讲,就分为DFSBFS,但是也有一些拓展的比如迭代深搜和IDA*搜索等等,搜索可以说是最基础却又是应用最广泛的算法。本文简单地总结下基础搜索在给定的显式图中的应用。

 

搜索与DP,贪心,分治的共同点在于状态的转移,只不过在搜索中,一个阶段的最优状态是由之前所有状态得到的,这是其余其他算法的区别之处。

 

状态的转移即由某一个不同的状态转移至另一个不同的状态,DFS在于从一个初始状态出发,一直转移状态直到搜到目标或搜到状态无法进行转移为止,其中的剪枝便是通过应题目具体要求和仔细分析而去掉某些没必要或不存在的状态以节约时间和空间,而BFS在于一层一层地扩展状态,这样首先找到的目标便一定是用时或步数最少的。DFS几乎适用于所有情况,因为这本身是一种遍历所有状态的搜索,只不过在寻找最小步或最小时间的情况下比较慢,无法快速找到目标,但在程序世界里DFS本身也有不适用的情况,如果状态没有下限即转移的深度没有下限那么便无法深搜,同样,BFS也可能存在一层状态都扩展不完的情况,这样便才会有迭代加深搜索等扩展搜索的出现,它结合了两者的优点且实际应用效果不错。紫书上面的名词解答树便很好地描述了所有状态转移的过程,当然这个似乎与我们无关,但是其实也是一种刻画搜索过程的有利工具。

 

下面便是我对搜索的心得与总结:

在具体的应用过程中:BFSDFS通常是各有优点且相互结合,ACM届总存在各种各样不同的题,你无法直观判断你的代码是否正确,因为搜索题大多不能自己去想例子来测试,只能通过题目所给的样例来初步加以评判,不管是你是否得到正确答案,也可能出现各种卡时间、卡空间的题,因为搜索本身的状态数量非常大,在解答题目时你需要明白一下几点:

 

1、如何进行状态的转移,简单地说怎么进行下一步的选择,这个通常很简单。

 2、是否需要标记已经转移过的状态以及怎么标记已经转移过的状态以不重复转移,你不能单一地判断,也就是要综合所给的题中所有出现的变化,在这个阶段中,我标记这个阶段时的状态,因为很可能不止在变化,图中的情况也在变化。

 3、怎么去除没必要的或不存在的状态,因为有些状态是不需要进行转移或扩展的。

 

弄清楚了以上三个问题,你就可以按题目要求从起始状态开始搜索了。

 

 在解题的过程中,我也遇到了很多错误,当然可能更多错误我没遇到或即将遇到。

 从两个简单的搜索题开始:

 

1  poj3009  Curling 2.0

 

题目大意要求把一个冰壶从起点“2”用最少的步数移动到终点“3”,其中0为移动区域,1为石头区域,冰壶一旦想着某个方向运动就不会停止,也不会改变方向(想想冰壶在冰上滑动),除非冰壶撞到石头1或者到达终点3,冰壶撞到石头后,冰壶会停在石头前面,此时(静止状态)才允许改变冰壶的运动方向,而该块石头会破裂,石头所在的区域由1变为0. 也就是说,冰壶撞到石头后,并不会取代石头的位置。终点是一个摩擦力很大的区域,冰壶若到达终点3,就会停止在终点的位置不再移动。

 

首先读懂题意:沿着某一行出发会遇到石头才会停止,不停止则失败,停止时石头也会消失,还有就是我的步数不能超过10步,这就明显告诉我们当我转移的次数大于10时直接去掉,而且在我从某一次状态转移回溯后所有的情况应该没变,所以之前在上一次转移给石头的标记应该去掉,然后再重新进行下一次同一深度的转移,这个题中状态去重很简单,只是走过的点标记就行了。所以综合以上分析,我只要不断深搜取得最小步数就行了,但是之前的细节考虑是必须的。(这个问题讨论BFS不可行是没意义的,两者都可以,虽说是寻找最小步数但是这个题目从搜索的过程来看DFS更适合)

 

代码:

#include <cstdio>int square[30][30];int w, h, sx, sy;int minnum;int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};void DFS(int x, int y, int cnt){    if (cnt >= 10) return ;    int i, nx, ny;    for (i = 0; i < 4; i++){        int flag = 1, tag = 0;        int nx = x+dir[i][0];        int ny = y+dir[i][1];        while (1){            if (nx < 1 || nx > h || ny < 1 || ny > w){                flag = 0;                break;            }            if (square[nx][ny] == 1){                tag = 1;                break;            }            if (square[nx][ny]==3 && minnum > ++cnt){                    minnum = cnt;                    return ;            }            nx += dir[i][0];            ny += dir[i][1];        }        if (!flag) continue;        if (tag){            square[nx][ny] = 0;            nx -= dir[i][0];            ny -= dir[i][1];            if (nx == x && ny == y){                square[nx+dir[i][0]][ny+dir[i][1]] = 1;                continue;            }            DFS(nx, ny, cnt+1);            square[nx+dir[i][0]][ny+dir[i][1]] = 1;        }    }}int main(){    int i, j;    while (scanf("%d %d", &w, &h), w+h){        minnum = 9999;        for (i = 1; i <= h; i++)            for (j = 1; j <= w; j++){                scanf("%d", &square[i][j]);                if (square[i][j] == 2){                    sx = i;                    sy = j;                }            }        DFS(sx, sy, 0);        if (minnum <= 10) printf("%d\n", minnum);        else puts("-1");    }    return 0;}


2  poj3669 MeteorShower


这个题目便是BFS水题了,不过也存在很多细节问题,做这个题的时候已经很久没刷题了,然后做题不加思考,出现了很多傻逼的错误…..

题意:流星雨会袭击地球上的某些点,且毁坏这个点的同时上下左右4个点也会被毁掉。输入给定流星雨的数量和袭击点的坐标以及时间,然后人从原点出发,必须走到不被毁坏的点才能活命,最短的逃跑时间是多少,不能逃跑输出-1.

 

直接广搜寻找最小时间就可以了,但是我犯了很多错误

错误:

1、没有看清题意,题目说人是一定会动的,而不是一直就待在原点,所以最少花费的时间不可能为0

2、我想既然在第一象限移动,想只考虑向上或向右的状态以去掉不必要的状态但是这是考虑问题不到位,虽然人是一直在第一象限内运动,但一定会向上或向右运动的想法是错误的,可以向右再向左返回,某些情况下这样花费的时间最少,所以在用bfs的时候也应考虑四个方向的状态。

3、在函数内部定义大数组,导致程序运行不能输入也不能正常运行即结束了,这样的大数组为局部变量,只能定义为全局的。

4、没有注意到某些流星破坏的点会有重复,所以导致其被破坏的时间为最后一个值了,对于一个点,其被破坏的时间应该是最早的那个时刻。

代码:

#include <cstdio>#include <cstring>#include <algorithm>using namespace std;int dir[5][2] = {{0, 0}, {1, 0}, {-1, 0}, {0, 1}, {0, -1}};int flag[310][310], flag1[310][310], time[310][310];struct po{    int x, y, T;    po(int a, int b, int c):x(a), y(b), T(c){}    po():x(0), y(0), T(0){}};struct queue{    int fro, rea;    po p[500000];}q;int solve(){    int i, T, tag = 1, x, y, ans = 0, j, M;    scanf("%d", &M);    memset(flag, 0, sizeof(flag));    memset(flag1, 0, sizeof(flag1));    memset(time, -1, sizeof(time));    for (i = 0; i < M; i++){        scanf("%d %d %d", &x, &y, &T);        for (j = 0; j < 5; j++){            int a = x+dir[j][0], b = y+dir[j][1];            if (a>=0 && b>=0){                if (time[a][b] >= 0) time[a][b] = min(T, time[a][b]);                else time[a][b] = T;                flag[a][b] = 1;            }        }    }    q.rea = q.fro = 0;    q.p[q.rea++] = po(0, 0, 0);    while (tag && q.fro!=q.rea){        x = q.p[q.fro].x, y = q.p[q.fro].y;        ans = q.p[q.fro++].T;        if (flag[x][y] && time[x][y]<=ans) continue;        if (!flag[x][y] && ans>0) return ans;        for (i = 1; i < 5; i++){            int a = x+dir[i][0], b = y+dir[i][1];            if (a>=0 && b>=0 && !flag1[a][b]){                q.p[q.rea++] = po(a, b, ans+1);                flag1[a][b] = 1;            }        }    }    return -1;}int main(){    printf("%d\n", solve());    return 0;}

接下来换几个稍微不同的题了:

3  hdu1254 推箱子

这个题目稍微有点意思,也让我知道状态转移不是那么想当然的或者说所谓的状态决不能单一地判断。题目意思我就不说了,应该都明白,这个题目相当于有两个物体在进行状态的变化,而且箱子在进行某一个方向的状态变化时人必须得能够到达箱子后面才能转移,这个倒是能够想到,BFS寻找扩展状态,DFS判断能否到达后面进行状态的扩展,但我犯的错误就是在标记去重的时候没有注意到箱子在处在某个状态时人的状态,直接简单地标记点即箱子的坐标,你标记状态时必须要考虑所有的变化!!!(因为没有考虑到这个,然后就一直WA……

代码:

#include <cstdio>#include <queue>#include <cstring>using namespace std;int m, n, bx, by, mx, my, tag, ex, ey;int dir[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};int mat[10][10], f[10][10], vis[10][10][10][10];struct po{    int x, y, step, sx, sy;    po(int a, int b, int c, int d, int e):x(a), y(b), step(c), sx(d), sy(e){}};void check(int x, int y){    if (x==ex && y==ey) tag = 0;    if (tag){        for (int i = 0; i<4 && tag; i++){            int nx = x+dir[i][0], ny = y+dir[i][1];            if (nx<1 || nx>m || ny<1 || ny>n || f[nx][ny] || mat[nx][ny]==1) continue;            f[nx][ny] = 1;            check(nx, ny);        }    }}int solve(){    queue <po> q;    q.push(po(bx, by, 0, mx, my));    memset(vis, 0, sizeof(vis));    while (!q.empty()){        po t = q.front();        bx = t.x, by = t.y, mx = t.sx, my = t.sy, q.pop();        if (mat[bx][by] == 3) return t.step;        for (int i = 0; i < 4; i++){            ex = bx-dir[i][0], ey = by-dir[i][1];            int nx = bx+dir[i][0], ny = by+dir[i][1];            if (nx<1 || nx>m || ny<1 || ny>n || mat[nx][ny]==1 || vis[nx][ny][ex][ey]) continue;            if (ex<1 || ex>m || ey<1 || ey>n || mat[ex][ey]==1) continue;            memset(f, 0, sizeof(f));            tag = f[mx][my] = f[bx][by] = 1;            check(mx, my);            if (tag) continue;            q.push(po(nx, ny, t.step+1, ex, ey));            vis[nx][ny][ex][ey] = 1;        }    }    return -1;}int main(){    int k, i, j;    scanf("%d", &k);    while (k--){        scanf("%d %d", &m, &n);        for (i = 1; i <= m; i++)            for (j = 1; j <= n; j++){                scanf("%d", &mat[i][j]);                if (mat[i][j] == 2) bx = i, by = j;                if (mat[i][j] == 4) mx = i, my = j;            }        printf("%d\n", solve());    }    return 0;}


再来个比较相似的题目,差不多,这个就是钥匙的变化了,不过这个有点技巧:状态压缩。(紫书上面所说的枚举子集便是状态压缩,这个还是比较不错的,一种很好的技巧)

 

4HDU1429胜利大逃亡(续)

题目是中文的,就不多说了

代码;

#include <cstdio>#include <queue>#include <cstring>#define P(x) push(x)using namespace std;struct po{    int x, y, time, bin;    po(int a, int b, int c, int d):x(a), y(b), time(c), bin(d){}};int dx[] = {1, -1, 0, 0}, dy[] = {0, 0, 1, -1};int sx, sy, T, m, n;int vis[25][25][1<<11];char mat[25][25];queue<po> q;int solve(){    memset(vis, 0, sizeof(vis));    while (!q.empty()) q.pop();    q.push(po(sx, sy, 0, 0));    vis[sx][sy][0] = 1;    while (!q.empty()){        po t = q.front(); q.pop();        int ans = t.time, b = t.bin;        if (ans >= T) return -1;        if (mat[t.x][t.y] == '^') return ans;        for (int i = 0; i < 4; i++){            int nx = t.x+dx[i], ny = t.y+dy[i], nb = b;            if (nx>m || nx<1 || ny>n || ny<1 || vis[nx][ny][b] || mat[nx][ny]=='*') continue;            if (mat[nx][ny]>='a' && mat[nx][ny]<='j') nb |= (1<<(mat[nx][ny]-32-'A'));            vis[nx][ny][nb] = 1;            if (mat[nx][ny]>='A' && mat[nx][ny]<='J'){                if (nb & (1<<(mat[nx][ny]-'A'))) q.P(po(nx, ny, ans+1, nb));                continue;            }            q.P(po(nx, ny, ans+1, nb));        }    }    return -1;}int main(){    while (~scanf("%d %d %d", &m, &n, &T)){        getchar();        for (int i = 1; i <= m; i++){            for (int j = 1; j <= n; j++){                scanf("%c", &mat[i][j]);                if (mat[i][j] == '@') sx = i, sy = j;            }            getchar();        }        printf("%d\n", solve());    }    return 0;}



最后再来两个比较有意思的题目,一个BFS,一个DFS,是我们学校队内的个人赛题目,然而我比赛时直接卡题,一卡题脑子就蒙了,然后都没做出来,主要是这两个题目都不同于一般的题,不过也是大同小异,关键是想清楚细节才行,个人感觉还是比较好的。(我本来就是组里面最菜的,卡题就直接悲剧了)

 

强烈建议可以做下这两个题目!!!

5HDU1180 诡异的楼梯

E - 诡异的楼梯

Time Limit:1000MS     MemoryLimit:65536KB     64bitIO Format:%I64d& %I64u

Submit Status Practice HDU 1180

Description

Hogwarts正式开学以后,Harry发现在Hogwarts,某些楼梯并不是静止不动的,相反,他们每隔一分钟就变动一次方向
比如下面的例子里,一开始楼梯在竖直方向,一分钟以后它移动到了水平方向,再过一分钟它又回到了竖直方向.Harry发现对他来说很难找到能使得他最快到达目的地的路线,这时Ron(Harry最好的朋友)告诉Harry正好有一个魔法道具可以帮助他寻找这样的路线,而那个魔法道具上的咒语,正是由你纂写的

 

Input

测试数据有多组,每组的表述如下: 
第一行有两个数,MN,接下来是一个MN列的地图,'*'表示障碍物,'.'表示走廊,'|'或者'-'表示一个楼梯,并且标明了它在一开始时所处的位置:'|'表示的楼梯在最开始是竖直方向,'-'表示的楼梯在一开始是水平方向.地图中还有一个'S'是起点,'T'是目标,0<=M,N<=20,地图中不会出现两个相连的梯子.Harry每秒只能停留在'.''S''T'所标记的格子内

 

Output

只有一行,包含一个数T,表示到达目标的最短时间
注意:Harry只能每次走到相邻的格子而不能斜走,每移动一次恰好为一分钟,并且Harry登上楼梯并经过楼梯到达对面的整个过程只需要一分钟,Harry从来不在楼梯上停留.并且每次楼梯都恰好在Harry移动完毕以后才改变方向

 

Sample Input

5 5

**..T

**.*.

..|..

.*.*.

S....

       

       

 

Sample Output

7

 

 

同样的,这个题目你在标记去重的时候不能单纯地考虑到人的位置,还要考虑到此时楼梯的状态,而且在人到达楼梯时如果能过去那么就直接扩展两步,如果不能人必须在这等待一分钟,等楼梯变回可以通过,这个时候也可以直接扩展,不过此时时间+2罢了,这相当于人在这等待一分钟然后花一分钟再走过去。

 

代码:

#include <cstdio>#include <queue>#include <algorithm>#include <cstring>using namespace std;struct po{    int x, y, time;    po(int a, int b, int c):x(a), y(b), time(c){}};queue <po> q;int m, n, sx, sy;int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};char maze[25][25];int vis[25][25][2];int solve(){    int ans, a, b, c;    while (!q.empty()) q.pop();    memset(vis, 0, sizeof(vis));    q.push(po(sx, sy, 0));    vis[sx][sy][0] = 1;    while (!q.empty()){        po t = q.front(); q.pop();        ans = t.time;        if (maze[t.x][t.y] == 'T') return ans;        for (int i = 0; i < 4; i++){            a = t.x+dx[i], b = t.y+dy[i], c = ans%2;            if (a>m || a<1 || b>n || b<1 || vis[a][b][c] || maze[a][b]=='*') continue;            if (i>1 && ((maze[a][b]=='|' && !c) || (maze[a][b]=='-' && c))){                b += dy[i];                if (b>n || b<1 || vis[a][b][c] || maze[a][b]=='*') continue;                q.push(po(a, b, ans+2));                vis[a][b][c] = 1;            }            if (i<2 && ((maze[a][b]=='-' && !c) ||(maze[a][b]=='|' && c))){                a += dx[i];                if (a>m || a<1 || vis[a][b][c] || maze[a][b]=='*') continue;                q.push(po(a, b, ans+2));                vis[a][b][c] = 1;            }            if (i<2 && ((maze[a][b]=='|' && !c ) || (maze[a][b]=='-' && c))) a += dx[i];            if (i>1 && ((maze[a][b]=='|' && c) || (maze[a][b]=='-' && !c))) b += dy[i];            if (a>m || a<1 || b>n || b<1 || vis[a][b][(ans+1)%2] || maze[a][b]=='*') continue;            q.push(po(a, b, ans+1));            vis[a][b][(ans+1)%2] = 1;        }    }    return ans;}int main(){    while (~scanf("%d %d", &m, &n)){        getchar();        for (int i = 1; i <= m; i++){            for (int j = 1; j <= n; j++){                scanf("%c", &maze[i][j]);                if (maze[i][j] == 'S') sx = i, sy = j;            }            getchar();        }        printf("%d\n", solve());    }    return 0;}


6codeforce197D Infinite Maze

 

 最后一个DFS题了,这个题真的很特别,这个题目还是两年前学校个人赛的题,当时也没人做出来,这次个人赛同样没人做出来。不过当我看了两年前的榜时,我佩服于那时年轻的wwdd(后面是学校ACM队长,深得教练喜爱,已经毕业退役了),前面的50分钟他切掉2题稳拿第一,在比赛整整的5个小时中后4个小时全部是在死磕这个题,总共错了46次,题目测试样例共100多组,他从第4组错到第93组,我觉得不能做出来是正常的,因为在强大的人总有失误的时候,但这也见证了他的执着与能力,更是体现了他在比赛时所具备的耐心与强大的心理素质。

当然这是题外话了,不过也是有感而发,下面是题目。

 

 

B - Infinite Maze

Time Limit:2000MS     MemoryLimit:262144KB     64bitIO Format:%I64d& %I64u

Submit Status Practice CodeForces 197D

Description

We've gotarectangular n × m-cell maze. Eachcell is either passable,or is a wall (impassable). A little boy found the mazeand cyclically tiled aplane with it so that the plane became an infinite maze.Now on this planecell (x, y) is a wall if and only ifcell  isa wall.

In thisproblem  isaremainder of dividing number a by number b.

The littleboystood at some cell on the plane and he wondered whether he can walkinfinitelyfar away from his starting position. From cell (x, y) hecan goto one of the following cells: (x, y - 1), (x, y + 1), (x - 1, y) and (x + 1, y),provided thatthe cell he goes to is not a wall.

Input

The firstlinecontains two space-separated integers n and m (1 ≤ n, m ≤ 1500)— the heightand the width of the maze that the boy used to cyclically tile theplane.

Each ofthenext n linescontains m characters— thedescription of the labyrinth. Each character is either a "#",thatmarks a wall, a ".", that marks a passable cell, or an"S",that marks the little boy's starting point.

Thestartingpoint is a passable cell. It is guaranteed that character "S"occursexactly once in the input.

Output

Print"Yes"(without the quotes), if the little boy can walk infinitely farfrom thestarting point. Otherwise, print "No" (without the quotes).

Sample Input

Input

5 4
##.#
##S#
#..#
#.##
#..#

Output

Yes

Input

5 4
##.#
##S#
#..#
..#.
#.##

Output

No

Hint

In thefirstsample the little boy can go up for infinitely long as there is a"clearpath" that goes vertically. He just needs to repeat thefollowing stepsinfinitely: up, up, left, up, up, right, up.

In thesecondsample the vertical path is blocked. The path to the left doesn't work,too —the next "copy" of the maze traps the boy.

 

题目大概意思就是说给定一个图,这个图可以无限地向上下左右四个方向复制,也就是说当我从原图的第一行向上走,本来是越界的,但是复制后相当于我走到了同列的最后一行,其他方向一样的考虑,最后问能否从给定的始点S出发走一条无限的路也就是可以一直走下去。

 

这个题目其实仔细想想也是不难的,因为如果我沿一条路走能够走到原来标记过的点那么就说明我可以一直走下去了,不需要排除越界后的状态,当我越界后便重新取余后回到原图,也相当于是在复制的图中一直走,标记原图中已经走过的状态,当我能够一直深搜能够回到已经标记过的点(也即两次坐标必须不同)

刚开始做的时候考虑欠妥,想来是先从原图中的点出发,如果后面走到的标记的点横纵坐标有一个大于原来的点时便符合,这个想必是错的,我可以先走出去即越界这时候就不符合了,而且我最开始做的时候单纯地以为图只能往下或往右复制,其实图可以往四周复制(某些测试样例中必须先向上或向左走才能走无限的路),所以这样的话我又错了,因为出现了负数。我犯的最后一个错就是判断坐标是否相同时我为了简单起见,判断和是否相同,然而我特地去CF上跑一遍以找到错误的样例,发现错误情况中刚好两个坐标和相同却又能走无限的路….

 

综合以上考虑,直接从始点出发,一直深搜,如果越界便通过取余的技巧回到原图(这是为了好判断越界后图的情况),如果走到已经标记过的点且坐标不同的话便证明可以走无限的路了。

 

代码:

(其实我的代码还是很挫的(239720kb1528ms),差点就MLETLE了,很多人都比我写的好,时间空间少了很多很多,可以去找更好的代码以参考)

#include <cstdio>#include <cstring>#include <vector>using namespace std;struct po{    int x, y;    po(int a, int b):x(a), y(b){}};int n, m, sx, sy, tag = 1;char maze[1510][1510];vector<po> vis[1510][1510];int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};void solve(int x, int y){    if (tag){        for (int i = 0; i<4 && tag; i++){            int nx = x+dx[i], ny = y+dy[i];            int a = (nx%n+n)%n?(nx%n+n)%n:n, b = (ny%m+m)%m?(ny%m+m)%m:m;            if (maze[a][b]=='#') continue;            if (vis[a][b].size()){                if(vis[a][b][0].x!=nx || vis[a][b][0].y!=ny){                    tag = 0;                }                else continue;            }            else vis[a][b].push_back(po(nx, ny));            solve(nx, ny);        }    }}int main(){    scanf("%d %d", &n, &m);    getchar();    for (int i = 1; i <= n; i++){        for (int j = 1; j <= m; j++){            scanf("%c", &maze[i][j]);            if (maze[i][j] == 'S') sx = i, sy = j;        }        getchar();    }    memset(vis, 0, sizeof(vis));    vis[sx][sy].push_back(po(sx, sy));    solve(sx, sy);    tag?puts("No"):puts("Yes");    return 0;}

最后,我试了试把vector换成数组以比较STL模板容器和自定义容器的区别,结果真的就MLE了,看来vector还真是好用………..

我想说的最后一句话就是刷题一定不能想当然,首先需要考虑仔细再下手,真的说各种样例来测你的代码难免会出现错误的情况,而且做题之前一定要把题目看清,我很多题目都因为是英文题没注意细节,然后无尽的WA

 

关于DFSBFS就讲到这里,希望我的总结对你有帮助<(^-^)>

 

0 0
原创粉丝点击