DFS初入门

目录

一、前言

二、搜索与暴力法

1、概念

2、搜索的基本思路

3、BFS:一群老鼠走迷宫

4、DFS:一只老鼠走迷宫

三、DFS

1、DFS访问示例

2、DFS的常见操作

3、DFS基础:递归和记忆化搜索

4、DFS的代码框架(大量编码后回头体会)

5、DFS:保护现场、恢复现场

6、DFS:搜索和输出所有路径

(1)模拟路径过程

(2)DFS搜索所有路径

(3)路径问题:BFS 和 DFS


一、前言

DFS 的本质就是递归,不同的是在递归的过程中加点料,由于递归的特殊操作模式,人脑很难模拟整个过程,而从我们熟知的排列数字和八皇后可以看出,基本套路是先枚举本层的所有可能,再进行递归,也就是枚举所有本层兄弟的可能,再向下走,而下一层也是这样的过程。关于是否回溯,其实只要是递归就要回溯,所以必定有返回值,有的人认为恢复现场就是回溯,其实是不正确的。关于是否需要恢复现场,如果后面操作涉及到前面的内容,则需要恢复现场,但是如果是树的重心这道题,则不需要,根本原因在于变量的作用范围,用变量记录了子树中节点的个数,每个节点遍历过程也不需要重复,所以不需要恢复现场。递归的逻辑自洽,是能够在某个节点结束递归(一般是在最后一层或者叶子结点),而且不管哪一层的操作过程都是相同的,就可以使用递归,这样理解树的重心会更加容易些。

二、搜索与暴力法

1、概念

搜索:“暴力法” 算法思想的具体实现。

搜索:“通用” 的方法。一个问题,如果比较难,那么先尝试一下搜索,或许能启发出更好的算法。

技巧:竞赛时遇到不会的难题,用搜索提交一下,说不定部分判题数据很弱,得分了!

【暴力法】

暴力法 (Brute force,又译为蛮力法)。

把所有可能性都列举出来,一一验证。简单直接!

利用计算机强大的计算能力和存储能力。

蛮力法也是一种重要的算法设计技术:

(1)理论上,蛮力法可以解决可计算领域的各种问题。

(2)蛮力法经常用来解决一些较小规模的问题。

(3)对于一些重要的问题蛮力法可以产生一些合理的算法,具备一些实用价值,而且不受问题规模的限制。

(4)蛮力法可以作为某类问题时间性能的底限,来衡量同样问题的更高效算法。

【蛮力的基本方法】⭐⭐⭐

蛮力的基本方法——扫描

关键——依次处理所有元素

基本的扫描技术——遍历

(1)集合的遍历

(2)线性表的遍历

(3)树的遍历

(4)图的遍历

2、搜索的基本思路

【BFS】

Breadth-First Search,宽度优先搜索,或称为广度优先搜索

【DFS】

Depth-First Search,深度优先搜索

3、BFS:一群老鼠走迷宫

  • 老鼠无限多;
  • 在每个路口,都派出部分老鼠探索所有没走过的路;
  • 走某条路的老鼠,如果碰壁无法前行,就停下;
  • 如果到达的路口已经有别的老鼠探索过了,也停下;
  • 所有的道路都会走到,而且不会重复。

DFS初入门_第1张图片

全面扩散、逐层递进

4、DFS:一只老鼠走迷宫

  • 只有一只老鼠;
  • 在每个路口,都选择先走右边(当然,选择先走左边也可以),能走多远就走多远;
  • 碰壁无法再继续往前走,回退一步,这一次走左边,然后继续往下走;
  • 能走遍所有的路,而且不会重复 (回退不算重复)。

DFS初入门_第2张图片

一路到底、逐层回退

三、DFS

1、DFS访问示例

设先访问左节点,后访问右节点

模拟老鼠走迷宫

访问顺序:{E B A D C G F I H}。

DFS初入门_第3张图片

2、DFS的常见操作

DFS的代码比BFS更简短。

DFS的主要操作:

        时间戳

                DFS序

                树深度

                子树节点数

                中序输出

                先序输出

                后序输出

3、DFS基础:递归和记忆化搜索

  • 形式上,递归函数是 “自己调用自己”,是一个不断 “重复” 的过程。
  • 递归的思想,是把大问题逐步缩小,直到变成最小的同类问题的过程,而最后的小问题的解是已知的,一般是给定的初始条件。
  • 到达最小问题后,再“回溯”,把小问题的解逐个带回给更大的问题,最终最大问题也得到了解决。
  • 递归有两个过程:递归前进、递归返回(回溯)。
  • 在递归的过程中,由于大问题和小问题的解决方法完全一样,那么大问题的代码和小问题的代码可以写成一样。
  • 一个递归函数,直接调用自己,实现了程序的复用。

例如:斐波那契数列

递推式:f(n)=f(n-1)+f(n-2)

打印第20个数:

fib=[0]*25
fib[1]=1
fib[2]=1
for i in range(3,21):
    fib[i]=fib[i-1]+fib[i-2]
print(fib[20])

改用递归实现:

def fib(n):
    global cnt
    cnt+=1
    if n==1 or n==2:
        return 1
    return fib(n-1)+fib(n-2)    #递归调用自己2次,复杂度O(2^n)

cnt=0
print(fib(20))
print(cnt)          #递归了cnt=13529次

递推和递归两种代码,结果一样,计算量差别巨大

递推代码:一个 for 循环,计算 20 次。

递归代码:计算第 20 个斐波那契数,共计算 cnt=13529 次。

为什么斐波那契的递归代码如此低效?

DFS初入门_第4张图片

 【原因】

代码低效的原因:return fib(n-1)+fib(n-2)

递归调用了自己 2 次,倍增计算 fib(n) 时,共执行了 O(2^n) 次递归

不过,很多递归函数只调用自己一次,不会额外增加计算量。

【改进:记忆化】

递归的过程中做了重复工作,例如 fib(3) 计算了 2 次,其实只算 1 次就够了。

为避免递归时重复计算,可以在子问题得到解决时,就保存结果,再次需要这个结果时,直接返回保存的结果就行了,不继续递归下去。这种存储已经解决的子问题结果的技术称为 “记忆化(Memoization)”。

记忆化是递归的常用优化技术。动态规划也常常用递归写代码,记忆化也是动态规划的关键技术。

import sys
sys.setrecursionlimit(30000)    #设置递归深度
def fib(n):
    global cnt
    cnt+=1
    if n==1 or n==2:
        data[n]=1
        return data[n]
    if data[n]!=0:
        return data[n]
    data[n]=fib(n-1)+fib(n-2)
    return data[n]
data=[0]*3005
cnt=0
print(fib(300))
print(cnt)

递归的关键问题:递归深度不能太大。

Python 默认递归深度 1000,如果递归深度太大,提示 "maximum recursion depth exceeded in comparison"。

用 sys.setrecursionlimit() 设置递归深度。

常常有深度大于 1000 的递归题目

4、DFS的代码框架(大量编码后回头体会)

DFS的框架,请在大量编码的基础上,再回头体会这个框架的作用。

DFS初入门_第5张图片

5、DFS:保护现场、恢复现场

在 DFS 框架中,最让初学者费解的是第 10 行和第 12 行。

第 10 行的 used[i]=1,称为“保存现场”,或“占有现场”。

第 12 行的 used[i]=0,称为“恢复现场”,或“释放现场”。

DFS初入门_第6张图片

6、DFS:搜索和输出所有路径

【题目描述】给出一张图,输出从起点到终点的所有路径。

【输入描述】第一行是整数 n, m,分别是行数和列数。后面 n 行,每行 m 个符号。'@' 是起点, '*' 是终点,'·' 能走,'#' 是墙壁不能走。在每一步,都按 左-上-右-下 的顺序搜索。在样例中,左上角坐标 (0,0),起点坐标(1,1),终点坐标 (0,2)。1

【输出描述】输出所有的路径。坐标 (i, j) 用 ij 表示,例如坐标 (0,2) 表示为 02。从左到右是 i,从上到下是 j。

【输入样例】

5 3

.#.

#@.

*..

...

#.#

【输出样例】

from 11 to 02

1:11->21->22->12->02

2:11->21->22->12->13->03->02

3:11->21->22->23->13->03->02

4:11->21->22->23->13->12->02

5:11->12->02

6:11->12->22->23->13->03->02

7:11->12->13->03->02

(1)模拟路径过程

【第一条路经】

  • 从起点 (1,1) 出发,查询它的 “左-上-右-下”哪个方向能走,发现 “左上” 不能走,可以走 “右”。
  • 第一步走到右边的 (2,1)。然后从 (2,1) 继续走,它可以向上走到 (2,0),但是后面就走不通了,退回来改走下面的 (2,2)。
  • 逐步深入走到终点,最后得到一条从起点 (1,1) 到终点 (0,2) 的路径 ''11->21->22->12->02" 。
  • 在这次DFS深入过程中,这条路径上曾经路过的点被 “保存现场”,不允许再次经过。
  • 到达终点后,从终点退回,回溯寻找下一个路径。退回后 “恢复现场”。

DFS初入门_第7张图片

【第二条路经】

搜到一条路径后,从终点 (0, 2) 退回到 (1, 2) ,继续走到 (1, 3)、(0, 3)、(0,  2)。

DFS初入门_第8张图片

【第三条路经】

  • 从终点 (0,2) 一路退回到 (2,2) 后,才找到新路径。
  • 在退回的过程中,原来被 “保存现场的” (0, 3)、(1, 3)、(0, 2),重新被 “恢复现场”,允许被经过。
  • 例如 (1, 3),在第二条路径中曾用过,这次再搜新路径时,在第三条路径中重新经过了它。
  • 如果不 “恢复现场”,这个点就不能在新路径中重新用了。

DFS初入门_第9张图片

【第四条路径】

DFS初入门_第10张图片

(2)DFS搜索所有路径

  • “保存现场” 的作用,是禁止重复使用。当搜索一条从起点到终点的路径时,这条路径上经过的点,不能重复经过,否则就兜圈子了,所以路径上的点需要“保存现场”,禁止再经过它。没有经过的点,或者碰壁后退回的点,都不能“保存现场”,这些点可能后面会进入这条路径。
  • “恢复现场” 的作用。当重新搜新的路径时,方法是从终点(或碰壁的点)沿着旧路径逐步退回,每退回一个点,就对这个点“恢复现场”,允许新路径重新经过这个点。例如上图的点 (1,3)。

DFS初入门_第11张图片

  • 路径有很多条,需要记录每条路径然后打印,这个代码使用了 “输出最短路径的简单方法”:每到一个点,就在这个点上记录从起点到这个点的路径。
  • P[][] 记录路径,p[i][j] 用字符串记录了从起点到点 (i,j) 的完整路径。
  • 第 13 行把新的点 (nx, ny) 加入到这个路径。这种“简单方法”浪费空间,适用于小图。
def dfs(x,y):
    global num
    for i in range(0,4):
        dir=[(-1,0),(0,-1),(1,0),(0,1)]     #左、上、右、下
        nx,ny=x+dir[i][0],y+dir[i][1]       #新坐标
        if nx<0 or nx>=hx or ny<0 or ny>=wy:    #不在地图范围内
            continue
        if mp[nx][ny]=='*':
            num+=1
            print("%d:%s->%d%d"%(num,p[x][y],nx,ny))    #打印路径
            continue            #不退出,继续找下一个路径
        if mp[nx][ny]=='.':
            mp[nx][ny]='#'      #保存现场。这个点在这次更深的dfs中不能再用
            p[nx][ny]=p[x][y]+'->'+str(nx)+str(ny)  #记录路径
            dfs(nx,ny)
            mp[nx][ny]='.'      #恢复现场,回溯之后,这个点可以再次使用
num=0
wy,hx=map(int,input().split())      #wy行,hx列。用num统计路径数量
a=['']*10
for i in range(wy):
    a[i]=list(input())      #读迷宫
mp=[['']*(10) for i in range(10)]       #二维矩阵mp[][]表示迷宫
for x in range(hx):
    for y in range(wy):
        mp[x][y]=a[y][x]
        if mp[x][y]=='@':   #起点
            sx=x
            sy=y
        if mp[x][y]=='*':   #终点
            tx=x
            ty=y
print("from %d%d to %d%d"%(sx,sy,tx,ty))
p=[['']*10 for i in range(10)]      #记录从起点到点path[i][j]的路径
p[sx][sy]=str(sx)+str(sy)
dfs(sx,sy)                          #搜索并输出所有的路径

(3)路径问题:BFS 和 DFS

搜所有的路径,应该用DFS;如果只搜最短路径,应该用BFS。

在一张图上,从起点到终点的所有路径数量是一个天文数字,读者可以用上面的代码试试一个 8×8 的图,看看路径总数是多少。但是搜最短的路径就比较简单,并不需要把所有路径搜出来之后再比较得到最短路,用BFS可以极快地搜到最短路。DFS适合用来处理暴力搜所有情况的题目

以上,DFS初入门

祝好

 

你可能感兴趣的:(蓝桥杯,算法,python,蓝桥杯,数据结构)