已收录此专栏。
今天我们会全面学习 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 个斐波那契数列为例,递归过程是这样的:
这个递归过程做了很多重复的工作啊,例如 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 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
但是!这个代码有一个致命缺点:不能按从小到大的顺序打印排列。而我们遇到的题目,常常需要按顺序输出排列。
所有还有另一种自写全排列算法
废话不多说,直接上代码。
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 迷宫的解说图:
无。
无。
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)
这就是本次所有内容,下期见。