目录
一、前言
二、搜索与暴力法
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 的本质就是递归,不同的是在递归的过程中加点料,由于递归的特殊操作模式,人脑很难模拟整个过程,而从我们熟知的排列数字和八皇后可以看出,基本套路是先枚举本层的所有可能,再进行递归,也就是枚举所有本层兄弟的可能,再向下走,而下一层也是这样的过程。关于是否回溯,其实只要是递归就要回溯,所以必定有返回值,有的人认为恢复现场就是回溯,其实是不正确的。关于是否需要恢复现场,如果后面操作涉及到前面的内容,则需要恢复现场,但是如果是树的重心这道题,则不需要,根本原因在于变量的作用范围,用变量记录了子树中节点的个数,每个节点遍历过程也不需要重复,所以不需要恢复现场。递归的逻辑自洽,是能够在某个节点结束递归(一般是在最后一层或者叶子结点),而且不管哪一层的操作过程都是相同的,就可以使用递归,这样理解树的重心会更加容易些。
搜索:“暴力法” 算法思想的具体实现。
搜索:“通用” 的方法。一个问题,如果比较难,那么先尝试一下搜索,或许能启发出更好的算法。
技巧:竞赛时遇到不会的难题,用搜索提交一下,说不定部分判题数据很弱,得分了!
【暴力法】
暴力法 (Brute force,又译为蛮力法)。
把所有可能性都列举出来,一一验证。简单直接!
利用计算机强大的计算能力和存储能力。
蛮力法也是一种重要的算法设计技术:
(1)理论上,蛮力法可以解决可计算领域的各种问题。
(2)蛮力法经常用来解决一些较小规模的问题。
(3)对于一些重要的问题蛮力法可以产生一些合理的算法,具备一些实用价值,而且不受问题规模的限制。
(4)蛮力法可以作为某类问题时间性能的底限,来衡量同样问题的更高效算法。
【蛮力的基本方法】⭐⭐⭐
蛮力的基本方法——扫描
关键——依次处理所有元素
基本的扫描技术——遍历
(1)集合的遍历
(2)线性表的遍历
(3)树的遍历
(4)图的遍历
【BFS】
Breadth-First Search,宽度优先搜索,或称为广度优先搜索
【DFS】
Depth-First Search,深度优先搜索
- 老鼠无限多;
- 在每个路口,都派出部分老鼠探索所有没走过的路;
- 走某条路的老鼠,如果碰壁无法前行,就停下;
- 如果到达的路口已经有别的老鼠探索过了,也停下;
- 所有的道路都会走到,而且不会重复。
全面扩散、逐层递进
- 只有一只老鼠;
- 在每个路口,都选择先走右边(当然,选择先走左边也可以),能走多远就走多远;
- 碰壁无法再继续往前走,回退一步,这一次走左边,然后继续往下走;
- 能走遍所有的路,而且不会重复 (回退不算重复)。
一路到底、逐层回退
设先访问左节点,后访问右节点
模拟老鼠走迷宫
访问顺序:{E B A D C G F I H}。
DFS的代码比BFS更简短。
DFS的主要操作:
时间戳
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 次。
为什么斐波那契的递归代码如此低效?
【原因】
代码低效的原因: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 的递归题目
DFS的框架,请在大量编码的基础上,再回头体会这个框架的作用。
在 DFS 框架中,最让初学者费解的是第 10 行和第 12 行。
第 10 行的 used[i]=1,称为“保存现场”,或“占有现场”。
第 12 行的 used[i]=0,称为“恢复现场”,或“释放现场”。
【题目描述】给出一张图,输出从起点到终点的所有路径。
【输入描述】第一行是整数 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) 出发,查询它的 “左-上-右-下”哪个方向能走,发现 “左上” 不能走,可以走 “右”。
- 第一步走到右边的 (2,1)。然后从 (2,1) 继续走,它可以向上走到 (2,0),但是后面就走不通了,退回来改走下面的 (2,2)。
- 逐步深入走到终点,最后得到一条从起点 (1,1) 到终点 (0,2) 的路径 ''11->21->22->12->02" 。
- 在这次DFS深入过程中,这条路径上曾经路过的点被 “保存现场”,不允许再次经过。
- 到达终点后,从终点退回,回溯寻找下一个路径。退回后 “恢复现场”。
【第二条路经】
搜到一条路径后,从终点 (0, 2) 退回到 (1, 2) ,继续走到 (1, 3)、(0, 3)、(0, 2)。
【第三条路经】
- 从终点 (0,2) 一路退回到 (2,2) 后,才找到新路径。
- 在退回的过程中,原来被 “保存现场的” (0, 3)、(1, 3)、(0, 2),重新被 “恢复现场”,允许被经过。
- 例如 (1, 3),在第二条路径中曾用过,这次再搜新路径时,在第三条路径中重新经过了它。
- 如果不 “恢复现场”,这个点就不能在新路径中重新用了。
【第四条路径】
- “保存现场” 的作用,是禁止重复使用。当搜索一条从起点到终点的路径时,这条路径上经过的点,不能重复经过,否则就兜圈子了,所以路径上的点需要“保存现场”,禁止再经过它。没有经过的点,或者碰壁后退回的点,都不能“保存现场”,这些点可能后面会进入这条路径。
- “恢复现场” 的作用。当重新搜新的路径时,方法是从终点(或碰壁的点)沿着旧路径逐步退回,每退回一个点,就对这个点“恢复现场”,允许新路径重新经过这个点。例如上图的点 (1,3)。
- 路径有很多条,需要记录每条路径然后打印,这个代码使用了 “输出最短路径的简单方法”:每到一个点,就在这个点上记录从起点到这个点的路径。
- 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) #搜索并输出所有的路径
搜所有的路径,应该用DFS;如果只搜最短路径,应该用BFS。
在一张图上,从起点到终点的所有路径数量是一个天文数字,读者可以用上面的代码试试一个 8×8 的图,看看路径总数是多少。但是搜最短的路径就比较简单,并不需要把所有路径搜出来之后再比较得到最短路,用BFS可以极快地搜到最短路。DFS适合用来处理暴力搜所有情况的题目
以上,DFS初入门
祝好