深度优先搜索剪枝(计蒜客-蓝桥杯国赛训练营)

深度优先搜索剪枝

视频学习链接: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} Ri1 H i H_i Hi< H i − 1 H_{i-1} Hi1
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积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=0m1Ri2π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=0m1RiHi
因为每个圆柱体的半径和高都是正整数,所以不能用数学办法直接求出最值,只能通过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} Ri1,而 R i R_i Ri必须为正整数,那么因为 R m − 1 R_{m-1} Rm1≥1,可以推出 R i R_i Ri≥m- i。
因此当i> 0时, R i R_i Ri∈[m-i, R i − 1 R_{i-1} Ri1-1].同理,H∈[m-i, H i − 1 H_{i-1} Hi1 - 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=im1RjHi=2/Rij=im1RiRjHi>=2/Rij=im1Rj2Hj=2(nv)/Ri=y

这样我们只需要在 s + 2 ( n − v ) / R i > = a n s s+2(n-v)/R_i>=ans s+2(nv)/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=im1RjHi=2/Rij=im1RiRjHi>=2/Rij=im1Rj2Hj=2(nv)/Ri=y

这样我们只需要在 s + 2 ( n − v ) / R i > = a n s s+2(n-v)/R_i>=ans s+2(nv)/Ri>=ans进行最优性剪枝。

菜鸟觉得太难了,代码写不动了。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值