DFS——剪枝优化&迭代加深

概述

优化搜索顺序

不同的搜索顺序会产生不同的搜索树形态,与可行性剪枝结合,去除非法状态,按照一定顺序可使规模大幅度减小。

例:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 : 输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]
在这里插入图片描述

可见第二种情况所访问的节点数量小于第一种,顺序的优化时为了尽快的减掉不合法的分支。也就是易于满足可行性剪枝的条件

排除等效冗余

在搜索过程中,如果我们能够判定从搜索树的当前节点上沿着某几条不同分支到达的子树是等效的,那么只需要对其中一条分支进行搜索。
一种是组合还是排列问题,如果是组合那么就上面的例题而言[1, 2]和[2, 1]是一种效果
其次就是路径选择时,前一个选择的路径和当前选择的路径效果相同,且前一个路径搜索失败,那么如果按照当前路径走的话效果相同,故可跳过。例如上题,搜索[2, 4, 4, 5]目标时7,如果搜索到第一个4,发现失败跳过后,搜索第二个失败状态也是一样的。如果成功的话对于组合数来说也是同等效果。

可行性剪枝

在搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界(非法),就执行回溯。
常见的剪枝是“上下界剪枝”

最优性剪枝

在最优化问题的搜索过程中,如果当前花费的代价已经超过了当前已经搜到的最优解,那么无论采取多么优秀的策略到达递归边界,都不可能更新答案。此时可以停止搜索,执行回溯。

例题

小猫爬山

翰翰和达达饲养了 N 只小猫,这天,小猫们要去爬山。

经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。

翰翰和达达只好花钱让它们坐索道下山。

索道上的缆车最大承重量为 W,而 N 只小猫的重量分别是 C1、C2……CN。

当然,每辆缆车上的小猫的重量之和不能超过 W。

每租用一辆缆车,翰翰和达达就要付 1 美元,所以他们想知道,最少需要付多少美元才能把这 N 只小猫都运送下山?

输入格式
第 1 行:包含两个用空格隔开的整数,N 和 W。

第 2…N+1 行:每行一个整数,其中第 i+1 行的整数表示第 i 只小猫的重量 Ci。

输出格式
输出一个整数,表示最少需要多少美元,也就是最少需要多少辆缆车。

数据范围
1≤N≤18,
1≤Ci≤W≤108
输入样例:
5 1996
1
2
1994
12
29
输出样例:
2

  1. 搜索顺序
    搜索每一个小猫,考虑小猫放到已有的缆车中还是新开一个缆车。
    终点:搜索完最后一个小猫,所以需要一个参数cur表示当前搜索的是第几只猫
    路径选择:新开一个缆车或是在已有缆车中选择,所以需要一个参数u表示已经开好的缆车
    所以参数def dfs(u, cur)

  2. 代码

N = 20

cab = [0] * N
cats = []
ans = N

def dfs(u, cur) :
	global ans
	if ans <= u : #最优性剪枝
		return
	if cur == n : 
		ans = u
		return
	
	cab[u] = cats[cur] #开一个新缆车
	dfs(u + 1, cur + 1)
	cab[u] = 0
	for i in range(u) :
		if cab[i] + cats[cur] <= m :#可行性剪枝
			cab[i] += cats[cur]
			dfs(u, cur + 1)
			cab[i] -= cats[cur]
			
n, m = map(int, input().split())
for i in range(n) : 
	t = int(input())
	cats.append(t)

cats.sort(reverse = True) #优化搜索顺序

dfs(0, 0)
print(ans)
  1. 剪枝的操作
    if cab[i] + cats[cur] <= m :#可行性剪枝,对应于可行性剪枝,缆车的承重有一定限制,那么从大到小搜索质量可以尽快的去处不合法的分支
木棒

乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。

然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。

请你设计一个程序,帮助乔治计算木棒的可能最小长度。

每一节木棍的长度都用大于零的整数表示。

输入格式
输入包含多组数据,每组数据包括两行。

第一行是一个不超过 64 的整数,表示砍断之后共有多少节木棍。

第二行是截断以后,所得到的各节木棍的长度。

在最后一组数据之后,是一个零。

输出格式
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。

数据范围
数据保证每一节木棍的长度均不大于 50。

输入样例:
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
输出样例:
6
5

  1. 搜索顺序
    枚举木棒长度,因为,木棍是由所有长度相等的木棒砍断而来,所以只有当木棒长度可以整除木棍长度之和,才可行。
    枚举能被木棍长度之和整除的木棒长度。假如此时枚举的长度为length,搜索木棍的组合,考虑木棍的组合的和是否等于length。如果相等,则除去当前搜索组合中的木棍,并记录木棒个数,从剩余木棍中搜索组合和为length的木棍。如果不相等则失败。搜索直到木棒的个数*length等于木棍长度之和为止
    终点:木棒的个数u * length等于木棍长度之和
    路径选择:组合数,需要有搜索的木棍的位置start
    可行性 : 当前组合之和cur + 枚举到木棍长度

  2. 代码

N = 70

def dfs(u, cur, start) :
	if length * u == cal : return True
	if cur == length : return dfs(u + 1, 0, 0)

	i = start
	while i < n :
		if not st[i] and cur + sticks[i] <= length : #可行性剪枝
			st[i] = True
			if dfs(u, cur + sticks[i], i + 1) : return True
			st[i] = False
			if not cur or cur + sticks[i] == length : return False #排除首尾冗余
			j = i + 1
			while j < n and sticks[i] == sticks[j] : j += 1 #排除路径冗余
			i = j - 1
		i += 1
			

while True :
	n = int(input())
	if n == 0 : break
	st = [False] * N
	sticks = list(map(int, input().split()))
	sticks.sort(reverse = True) # 优化搜索顺序
	cal = sum(sticks)
	length = 1
	while True :
		if cal % length == 0 and dfs(0, 0, 0) :
			break
		length += 1
	print(length)
  1. 剪枝
    除了上面提到的可行性剪枝,本题值得注意的是等效冗余剪枝

在这里插入图片描述

迭代加深

概述

深度优先搜索每次都是选定一个分支,不断深入,直至到达递归边界才回溯。但是这种策略带有一定的缺陷。试想以下情况:搜索树每个节点的分支数目非常多,并且问题的答案在某个较浅的节点上。如果深搜一开始选错了分支,就很可能在不包含答案的深层子树上浪费许多时间。
如下图是问题的状态空间,五角星标识着答案:
在这里插入图片描述
那么深度优先搜索算法产生的搜索树就如下图所示,该算法在矩形圈出的深层子树上浪费了很多时间。
在这里插入图片描述
此时,我们可以从小到大限制搜索的深度,如果在当前深度限制下搜索不到答案,就把搜索深度限制增加,扩大一层,然后重新进行一次搜索,这就是迭代加深的思想。所谓"迭代",就是以上一次的结果为基础,重复执行以逼近答案。

迭代加深DFS的过程如下:
在这里插入图片描述
总之,当搜索树的规模随着层次的深入增长很快,并且我们能够确保答案在一个较浅的节点时,就可以采用迭代加深DFS的方法。某些题目描述"若10步以内搜不到结果就算无解",对于这种题目,可以大胆用迭代加深DFS算法。

加成序列

满足如下条件的序列 X(序列中元素被标号为 1、2、3…m)被称为“加成序列”:

X[1]=1
X[m]=n
X[1]<X[2]<…<X[m−1]<X[m]
对于每个 k(2≤k≤m)都存在两个整数 i 和 j (1≤i,j≤k−1,i 和 j 可相等),使得 X[k]=X[i]+X[j]。
你的任务是:给定一个整数 n,找出符合上述条件的长度 m 最小的“加成序列”。

如果有多个满足要求的答案,只需要找出任意一个可行解。

输入格式
输入包含多组测试用例。

每组测试用例占据一行,包含一个整数 n。

当输入为单行的 0 时,表示输入结束。

输出格式
对于每个测试用例,输出一个满足需求的整数序列,数字之间用空格隔开。

每个输出占一行。

数据范围
1≤n≤100
输入样例:
5
7
12
15
77
0
输出样例:
1 2 4 5
1 2 4 6 7
1 2 4 8 12
1 2 4 5 10 15
1 2 4 8 9 17 34 68 77

  1. 搜索策略:
    依次搜索序列中每个位置k,枚举i和j作为分支,把X[i] + X[j]的和填到X[k]上,然后递归填写下一个位置

  2. 代码

N = 110
path = [0] * N

def dfs(u, k) :
	if u == k : return path[u - 1] == n
	st = [False] * N #去除等效冗余
	for i in range(u - 1, -1, -1) :
		for j in range(i, -1, -1) :
			s = path[i] + path[j]
			if s > n or s <= path[u - 1] or st[s] : continue #可行性剪枝
			st[s] = True
			path[u] = s
			if dfs(u + 1, k) : return True
	return False

while True :
	n = int(input())
	if n == 0 : break
	path[0] = 1
	depth = 1
	while not dfs(1, depth) : depth += 1 #迭代加深
	for i in range(depth) :
		print(path[i], end = " ")
	print()

总结

一般题目都会有可行性剪枝,其他几个如何剪枝需要依据题意进行剪枝。

当搜索树的规模随着层次的深入增长很快,并且我们能够确保答案在一个较浅的节点时,就可以采用迭代加深DFS的方法。某些题目描述"若10步以内搜不到结果就算无解",对于这种题目,可以大胆用迭代加深DFS算法。

posted @ 2022-12-22 20:53  chanxe  阅读(174)  评论(0编辑  收藏  举报