文章目录
深度优先搜索剪枝
视频学习链接:https://www.bilibili.com/video/BV1pk4y1z76B?p=2
我们前面已经学习到搜索过程最后会生成一颗搜索树。
剪枝,顾名思义,就是通过一些判断,砍掉搜索树上不必要的子树。有时候,我们会发现某个结点对应的子树的状态都不是我们要的结果,那么我们其实没必要对这个分支进行搜索,砍掉这个子树,就是剪枝。
可行性剪枝
之前讨论过的问题:给定n个整数,要求选出K个数,使得选出来的K个数的和为sum
如上图,当k= 2的时候,如果已经选了2个数,再往后选多的数是没有意义的。所以我们可以直接减去这个搜索分支,对应上图中的剪刀减去的那个子树。
又比如,如果所有的数都是正数,如果一旦发现当前的和值都已经大于sum了,那么之后不管怎么选和值都不可能回到sum了,我们也可以直接终止这个分支的搜索。
我们在搜索过程中,一旦发现如果某些状态无论如何都不能找到最终的解,就可以将其“剪枝”了。
类型例题:从n个数中选k个数,使得和是m
dfs
从0,1,2,…,29这30个数中选8个数出来,使得和值为200。
# 1选数和为200
# 从0,1,2,..,29这30个数中选8个数出来,使得和值为200。
# i从第一位开始,cnt当前选择了几个数,s选择了的数的和
def dfs(i,cnt,s):
if i == n :
if cnt == k and s == sumnum:
# 修改全局变量先声明
global ans
ans = ans + 1
return 1
# 有两种可能:1.不选直接进入下一个 2.选择该数字,进入下一个
dfs(i+1,cnt,s)
dfs(i+1,cnt+1,s+a[i])
n = 30
k = 8
sumnum = 200
# 数据存入数组
a = []
for i in range(0,30):
a.append(i)
print(a)
ans = 0
dfs(0,0,0)
print(ans)
运行时间很长,对其进行剪枝,缩短计算机运行计算的时间
深度优先搜索(dfs)剪枝
# 1选数和为200
def dfs(i,cnt,s):
# --------剪枝部分
if cnt > k:
return 1
if s > sumnum:
return 1
# --------剪枝部分
if i == n :
if cnt == k and s == sumnum:
# 修改全局变量先声明
global ans
ans = ans + 1
return 1
# 有两种可能:1.不选直接进入下一个 2.选择该数字,进入下一个
dfs(i+1,cnt,s)
dfs(i+1,cnt+1,s+a[i])
测试结果
数据在0-29之间选择8个数:5
数据在1-30之间选择8个数:70
最优性剪枝
对于求最优解的一类问题,通常可以用最优性剪枝,比如在求解迷宫最短路的时候,如果发现当前的步数已经超过了当前最优解,那从当前状态开始的搜索都是多余的,因为这样搜索下去永远都搜不到更优的解。通过这样的剪枝,可以省去大量冗余的计算。
此外,在搜索是否有可行解的过程中,一旦找到了一组可行解,后面所有的搜索都不必再进行了,这算是最优性剪枝的一个特例。
有一个n x m大小的迷宫。其中字符’S’表示起点,字符’T’表示终点,字符’*’ 表示墙壁,字符’.’ 表示平地。你需要从’S’出发走到’T’,每次只能向上下左右相邻的位置移动,并且不能走出地图,也不能走进墙壁。保证迷宫至少存在一种可行的路径,输出’S’走到’T’的最少步数。
通常我们会用BFS(广度优先搜索)解决这个问题,搜到的第一个结果就是答案。
现在我们考虑用DFS(深度优先搜索)来解决这个问题,第一个搜到的答案ans并不一定是正解,但是正解一定小于等于ans.于是如果当前步数大于等于ans就直接剪枝,并且每找到一个可行的答案,都会更新ans.
类型例题:迷宫问题(步数最少)
# 2迷宫问题剪枝
# 定义用到的全局变量
# 行 列
n , m = [int(i) for i in input("").split()]
# 存放迷宫的地图形式
maze = []
for i in range(0,n):
list1 = list(input(""))
maze.append(list1)
# 存放地图的位置是否走过,标记
vis = []
for i in range(0,n):
list1 = [0]*m
vis.append(list1)
# 默认步数
ans = 1000
def dfs(x,y,step):
global ans
# ----------剪枝部分
if step >= ans:
return 1
# -----------剪枝部分
if maze[x][y] == 'T':
if step < ans:
ans = step
return 1
# 进行标记
vis[x][y] = 1
maze[x][y] = 'm'
# 方向存入数组(顺序逆时针)
direction = [[-1,0],[0,-1],[1,0],[0,1]]
for i in range(0,len(direction)):
tx = x + direction[i][0]
ty = y + direction[i][1]
if tx >= 0 and tx < n and ty >= 0 and ty < m and maze[tx][ty] != '*' and vis[tx][ty] == 0:
dfs(tx,ty,step+1)
#若没有找到,对相关标记进行取消
vis[x][y] = 0
maze[x][y] = '.'
return 0
# 寻找起始位置
for i in range(0,n):
for j in range(0,m):
if maze[i][j] == "S":
x = i
y = j
dfs(x,y,0)
print(ans)
重复性剪枝
对于某一些特定的搜索方式,一个方案可能会被搜索很多次,这样是没必要的。
再来看这个问题:
给定n个整数,要求选出K个数,使得选出来的K个数的和为sum。
如果搜索方法是每次从剩下的数里选一个数,一共搜到第k层,那么1, 2, 3这个选取方法能被搜索到6次,这是没必要的,因为我们只关注选出来的数的和,而根本不会关注选出来的数的顺序,所以这里可以用重复性剪枝。
#3选数和为200剪枝
# 从0,1,2,..,29这30个数中选8个数出来,使得和值为200。
# s是和,cnt是选的数的计数个数,pos是上一次选取的数的位置
def dfs(s,cnt,pos):
if s > sumnum or cnt > k:
return 1
if s == sumnum and cnt == k:
global ans
ans = ans + 1
for i in range(pos,n):
if xuan[i] == 0:
xuan[i] = 1
dfs(s + a[i], cnt + 1 , i+1)
xuan[i] = 0
n = 30
k = 8
sumnum = 200
# 数据存入数组
a = []
xuan = []
for i in range(0,30):
a.append(i+1)
xuan.append(0)
print(a)
ans = 0
dfs(0,0,0)
print(ans)
#测试代码有问题
奇偶性剪枝
自己学习得不是很清楚
引爆炸弹练习题
引爆炸弹题目
在一个n x m的方格地图上,某些方格上放置着炸弹。手动引爆一个炸弹以后,炸弹会把炸弹所在的行和列上的所有炸弹引爆,被引爆的炸弹又能引爆其他炸弹,这样连锁下去。
现在为了引爆地图上的所有炸弹,需要手动引爆其中一些炸弹,为了把危险程度降到最低,请算出最少手动引爆多少个炸弹可以把地图上的所有炸弹引爆。
地图中,1表示有炸弹,0表示没有炸弹
讲解
我们把能相互引爆的炸弹放到一个集合中,那么最终划分出的集合个数就是我们要求的答案。显然,引爆一个集合中任意一个炸弹造成的效果都一样,我们可以遍历n x m的方格中所有炸弹,如果当前炸弹之前没被引爆,那就引爆它,即标记它所在集合中的所有炸弹。
可以用DFS搜出一个炸弹所在的集合,每次引爆一个炸弹后,就搜它所在的行列是否有其他炸弹,然后继续搜索。因为炸弹最多有nx m个,每个炸弹引爆后都要检查所在行和列n + m个格子有无炸弹,时间复杂度O(nm(n + m))。
这里有一个剪枝,每行每列最多只需要搜一遍,于是可以标记一下搜过的行列避免重复的搜索, 这样每个格子都最多被检查两遍,时间复杂度变为O(nm)。
代码
# 4引爆炸弹
def boom(x,y):
# 表示1为炸弹的地方访问过
# 如果不标记,会引起之后的行列搜索的时候,该位置反复引爆
maze[x][y] = 0
# 对行标记
if row[x] == 0:
# 表示该行被引爆
row[x] = 1
# 查找地图中该行中的炸弹,并且引爆
for i in range(0,m):
if maze[x][i] == 1:
boom(x,i)
# 对列标记
if col[y] == 0:
# 表示该列被引爆
col[y] = 1
# 查找地图中该列中的炸弹,并且引爆
for j in range(0,n):
if maze[j][y] == 1:
boom(j,y)
# 行 列
n , m = [int(i) for i in input("").split()]
# 炸弹地图
maze = []
# 行数组
row = []
# 列数组
col = []
for i in range(0,n):
list1 = [int(j) for j in list(input(""))]
maze.append(list1)
for i in range(0,n):
row.append(0)
for i in range(0,m):
col.append(0)
cnt = 0
for i in range(0,n):
for j in range(0,m):
if maze[i][j] == 1:
cnt = cnt + 1
boom(i,j)
print(cnt)
print(maze)
print(row)
print(col)
生日蛋糕练习题
生日蛋糕题目
今天是花椰妹的生日,蒜头君打算制作一个体积为nπ的m层生日蛋糕送给她。蛋糕每层都是一个圆柱体,设从下往上数第i(0≤i < m)层蛋糕是半径为Ri,高度为Hi的圆柱。当i> 0时,要求
R
i
R_i
Ri<
R
i
−
1
R_{i-1}
Ri−1且
H
i
H_i
Hi<
H
i
−
1
H_{i-1}
Hi−1。
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积Q最小。令Q = Sπ,请编程对给出的n和m,找出蛋糕的制作方案(适当的
R
i
R_i
Ri和
H
i
H_i
Hi的值),使S最小。(除 Q外,以上所有数据皆为正整数)
讲解
为了方便叙述,本题的表面积和体积都是除以π之后的结果,且表面积是指除去下底面后的表面积。
整个蛋糕的体积
n
π
=
∑
i
=
0
m
−
1
R
i
2
∗
π
∗
H
i
nπ=\sum_{i=0}^{m-1} R^2_i*π*H_i
nπ=∑i=0m−1Ri2∗π∗Hi
表面积
Q
=
R
0
2
∗
π
+
2
π
∑
i
=
0
m
−
1
R
i
∗
H
i
Q=R_0^2*π+2π\sum_{i=0}^{m-1}R_i*H_i
Q=R02∗π+2π∑i=0m−1Ri∗Hi
因为每个圆柱体的半径和高都是正整数,所以不能用数学办法直接求出最值,只能通过DFS搜索的办法寻找答案。
容易发现,除了第0层以外,在体积一定的情况下,
R
i
R_i
Ri越大, 表面积就越大。这样我们在搜索的过程中,会从大到小枚举
R
i
R_i
Ri,更有利于之后的剪枝优化。
现在我们来考虑第i层时,
R
i
R_i
Ri和
H
i
H_i
Hi的枚举范围。
题目要求
R
i
R_i
Ri <
R
i
−
1
R_{i-1}
Ri−1,而
R
i
R_i
Ri必须为正整数,那么因为
R
m
−
1
R_{m-1}
Rm−1≥1,可以推出
R
i
R_i
Ri≥m- i。
因此当i> 0时,
R
i
R_i
Ri∈[m-i,
R
i
−
1
R_{i-1}
Ri−1-1].同理,H∈[m-i,
H
i
−
1
H_{i-1}
Hi−1 - 1]。
最底层
R
0
<
=
n
R_0<=\sqrt{n}
R0<=n,
H
0
<
=
n
H_0<=n
H0<=n.
这样,我们就对每层枚举的范围进行了剪枝。
有些时候,当前情况的体积太大,导致即使后面几层的体积取到最小,也无法使最终体积等于n。
我们开一一个数组va预处理从上往下数前i个圆柱体最小的体积和,显然va[]等于
∑
j
=
1
i
j
3
\sum_{j=1}^ij^3
∑j=1ij3。那么我们在搜索中,就可以利用这个va数组进行可行性剪枝。
下面我们考虑最优性剪枝。如果已经搜到了一个答案ans,而当前表面积为s,当前体积为v,若s + x >= ans, 就能进行剪枝。
其中x为第i层到第m - 1层的最小表面积,我们在没搜索之前是无法计算出这个x,但是能根据当前局面估计出一个值y,满足y≤x。我们在s十y≥ans时进行剪枝,显然y越接近x,剪枝效果就越好。
下面我们通过不等式的放缩来找到这个y:
当前在第i层时,因为
x
=
2
∑
j
=
i
m
−
1
R
j
∗
H
i
=
2
/
R
i
∗
∑
j
=
i
m
−
1
R
i
∗
R
j
∗
H
i
>
=
2
/
R
i
∗
∑
j
=
i
m
−
1
R
j
2
∗
H
j
=
2
(
n
−
v
)
/
R
i
=
y
x=2\sum_{j=i}^{m-1}R_j*H_i=2/R_i*\sum_{j=i}^{m-1}R_i*R_j*H_i>=2/R_i*\sum_{j=i}^{m-1}R_j^2*H_j=2(n-v)/R_i=y
x=2j=i∑m−1Rj∗Hi=2/Ri∗j=i∑m−1Ri∗Rj∗Hi>=2/Ri∗j=i∑m−1Rj2∗Hj=2(n−v)/Ri=y
这样我们只需要在 s + 2 ( n − v ) / R i > = a n s s+2(n-v)/R_i>=ans s+2(n−v)/Ri>=ans进行最优性剪枝。
x >= ans, 就能进行剪枝。
其中x为第i层到第m - 1层的最小表面积,我们在没搜索之前是无法计算出这个x,但是能根据当前局面估计出一个值y,满足y≤x。我们在s十y≥ans时进行剪枝,显然y越接近x,剪枝效果就越好。
下面我们通过不等式的放缩来找到这个y:
当前在第i层时,因为
x
=
2
∑
j
=
i
m
−
1
R
j
∗
H
i
=
2
/
R
i
∗
∑
j
=
i
m
−
1
R
i
∗
R
j
∗
H
i
>
=
2
/
R
i
∗
∑
j
=
i
m
−
1
R
j
2
∗
H
j
=
2
(
n
−
v
)
/
R
i
=
y
x=2\sum_{j=i}^{m-1}R_j*H_i=2/R_i*\sum_{j=i}^{m-1}R_i*R_j*H_i>=2/R_i*\sum_{j=i}^{m-1}R_j^2*H_j=2(n-v)/R_i=y
x=2j=i∑m−1Rj∗Hi=2/Ri∗j=i∑m−1Ri∗Rj∗Hi>=2/Ri∗j=i∑m−1Rj2∗Hj=2(n−v)/Ri=y
这样我们只需要在 s + 2 ( n − v ) / R i > = a n s s+2(n-v)/R_i>=ans s+2(n−v)/Ri>=ans进行最优性剪枝。
菜鸟觉得太难了,代码写不动了。