蓝桥杯精选赛题算法系列——迷宫——DFS

已收录此专栏。
今天我们会全面学习 DFS 的相关知识,包括理论、模板、真题等。
深度优先搜索(DFS, Depth-First Search)和宽度优先搜索(BFS, Breadth-First Search,或称为广度优先搜索)是基本的暴力技术,常用于解决图、树的遍历问题。

我们以老鼠走迷宫为例说明 BFS 和 DFS 的原理吧。迷宫内的路错综复杂,老鼠从入口进去后,怎么才能找到出口?有两种方案:

1.一只老鼠走迷宫。它在每个路口,都选择先走右边(当然,选择先走左边也可以),能走多远就走多远;直到碰壁无法再继续往前走,然后往回退一步,这一次走左边,然后继续往下走。用这个办法,只要没遇到出口,就会走遍所有的路,而且不会重复(这里规定回退不算重复走)。这个思路就是 DFS。

2.一群老鼠走迷宫。假设老鼠无限多,这群老鼠进去后,在每个路口,都派出部分老鼠探索所有没走过的路。走某条路的老鼠,如果碰壁无法前行,就停下;如果到达的路口已经有别的老鼠探索过了,也停下。很显然,在遇到出口前,所有的道路都会走到,而且不会重复。这个思路就是 BFS。

在具体编程时,一般用队列这种数据结构来实现 BFS ,即 “BFS = 队列”;而 DFS 一般用递归实现,即 “DFS = 递归”。

递归

递归是初学编程时的“第一个拦路虎”,我想很多人理解起来有困难。不过现在从形式上看,递归函数就是“自己调用自己”,是一个不断“重复”的过程。

递归的算法思想,是把大问题逐步缩小,直到变成最小的同类问题的过程,而最后的小问题的解是已知的,一般是给定的初始条件。在递归的过程中,由于大问题和小问题的解决方法完全一样,那么大问题的代码和小问题的代码可以写成一样。
下面我们以斐波那契数列的计算为例,写写代码吧。
首先我们知道它的递推关系式是 f(n) = f(n-1) + f(n-2)。那么要打印第 2020个数,递推代码如下:

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

根据递推式改用递归编码,代码如下:

cnt = 0
def fib(n):
    global cnt 
    cnt += 1
    if n == 1 or n==2:
        return 1
    return fib(n-1) + fib(n-2)
print(fib(20))
print(cnt)

我们来分析一下递归过程吧。以计算第 5 个斐波那契数列为例,递归过程是这样的:
蓝桥杯精选赛题算法系列——迷宫——DFS_第1张图片
这个递归过程做了很多重复的工作啊,例如 fib(3) 计算了 2 次,但其实只算 1 次就够了。
在 fib(3) 函数中,它调用了自己 2 次。计算 fib(n) 时,计算量十分惊人。我在函数中用 cnt 统计了递归次数,计算第 20个斐波那契数时,cnt=13529。
为避免递归时重复计算子问题,可以在子问题得到解决时,就保存结果,再次需要这个结果时,直接返回保存的结果就行了。这种存储已经解决的子问题结果的技术称为“记忆化(Memoization)”。记忆化是递归的常用优化技术,以下是用“记忆化+递归”重写的斐波那契:

cnt = 0
data = [0 for _ in range(25)]
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]
print(fib(20))
print(cnt)

让我们来看看递归是如何解决排列相关问题的。
首先我们需要明确排列相关的是啥问题?
给出一些数,生成它们的排列,这是常见的需求,在蓝桥杯题目中常常出现。我们在“蓝桥杯精选赛题系列——暴力拼数”中给出了求排列的系统函数permutations(),还有印象吧?
系统函数permutations(),虽然好用,但是我们不能完全依赖于它。因为并不是所有需要排列的场景都能直接用 next_permutation(),有时候还是得自己写排列。一会的例题“寒假作业”,如果用 next_permutation() 函数会超时,所以必须得自己写排列算法。下面来分析一下两种自写代码的思路吧!

自写全排列算法1

1.我们设数字是 {1 2 3 4 5…n},那么递归求全排列的思路是:
让第一个数不同,得到 n 个数列。其办法是:把第 1 个和后面每个数交换。

1 2 3 4 5…n
2 1 3 4 5…n

n 2 3 4 5…1

以上 n 个数列,只要第一个数不同,不管后面n−1 个数是怎么排列的,这 n​ 个数列都不同。 这是递归的第一层。
2.继续:在上面的每个数列中,去掉第一个数,对后面的 n-1​ 个数进行类似的排列。例如从上面第 2 行的{2 1 3 4 5…n}进入第二层(去掉首位 2):

1 3 4 5…n
3 1 4 5…n

n 3 4 5…1

以上 n-1 个数列,只要第一个数不同,不管后面 n-2 个数是怎么排列的,这 n-1 个数列都不同。

这是递归的第二层。
3.重复以上步骤,直到用完所有数字。
代码如下:

a = [1,2,3,4,5,6,7,8,9,10,11,12,13]
def dfs(s,t):
    if s == t:
        for i in range(t+1):
            print(a[i],end = '')
        print('\n')
        return
    for  i  in range(s,t+1):
        a[s],a[i] = a[i],a[s]
        dfs(s+1,t)
        a[s],a[i] = a[i],a[s]

n = 3
dfs(0,n-1)

上面的代码输出的结果为:

1 2 3
1 3 2
2 1 3
2 3 1
3 2 1
3 1 2

但是!这个代码有一个致命缺点:不能按从小到大的顺序打印排列。而我们遇到的题目,常常需要按顺序输出排列。
所有还有另一种自写全排列算法

自写全排列算法2

废话不多说,直接上代码。

a = [1,2,3,4,5,6,7,8,9,10,11,12,13]
vis = [0 for _ in range(20)]
b = [0 for _ in range(20)]
def dfs(s,t):
    if s == t:
        for i in range(t):
            print(b[i],end = '')
        print('\n')
        return
    for  i  in range(t):
        if  not  vis[i]:
             vis[i]=True 
             b[s]=a[i]
             dfs(s+1,t)
             vis[i]=False


n = 3
dfs(0,n)

我们来一道题来实战一下:

题目描述

X 星球的一处迷宫游乐场建在某个小山坡上。它是由 10×10 相互连通的小房间组成的。

房间的地板上写着一个很大的字母。我们假设玩家是面朝上坡的方向站立,则:

L 表示走到左边的房间
R 表示走到右边的房间
U 表示走到上坡方向的房间
D 表示走到下坡方向的房间。

X 星球的居民有点懒,不愿意费力思考。他们更喜欢玩运气类的游戏。这个游戏也是如此!

开始的时候,直升机把 100 名玩家放入一个个小房间内。玩家一定要按照地上的字母移动。

迷宫地图如下:

UDDLUULRUL
UURLLLRRRU
RRUURLDLRD
RUDDDDUUUU
URUDLLRRUU
DURLRLDLRL
ULLURLLRDU
RDLULLRDDD
UUDDUDUDLL
ULRDLUURRR

请你计算一下,最后,有多少玩家会走出迷宫,而不是在里边兜圈子?

如果你还没明白游戏规则,可以参看下面一个简化的 4x4 迷宫的解说图:蓝桥杯精选赛题算法系列——迷宫——DFS_第2张图片

输入描述

无。

输出描述

无。

详解代码

move=[['U', 'D', 'D', 'L', 'U', 'U', 'L', 'R', 'U', 'L'], ['U', 'U', 'R', 'L', 'L', 'L', 'R', 'R', 'R', 'U'], ['R', 'R', 'U', 'U', 'R', 'L', 'D', 'L', 'R', 'D'], ['R', 'U', 'D', 'D', 'D', 'D', 'U', 'U', 'U', 'U'], ['U', 'R', 'U', 'D', 'L', 'L', 'R', 'R', 'U', 'U'], ['D', 'U', 'R', 'L', 'R', 'L', 'D', 'L', 'R', 'L'], ['U', 'L', 'L', 'U', 'R', 'L', 'L', 'R', 'D', 'U'], ['R', 'D', 'L', 'U', 'L', 'L', 'R', 'D', 'D', 'D'], ['U', 'U', 'D', 'D', 'U', 'D', 'U', 'D', 'L', 'L'], ['U', 'L', 'R', 'D', 'L', 'U', 'U', 'R', 'R', 'R']]
def tes (y,x,a):
    if y<0 or y>9 or x<0 or x>9:
        return 1
    elif a>100:
        return 0
    if move[y][x]=='U':
        return tes(y-1,x,a+1)
    elif move[y][x]=='D':
        return tes(y+1,x,a+1)
    elif move[y][x]=='R':
        return tes(y,x+1,a+1)
    else:
        return tes(y,x-1,a+1)
res=0
for y in range(10):
    for x in range(10):        
        res+=tes(y,x,0)
print(res)



这就是本次所有内容,下期见。

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