回溯问题Python框架总结——排列组合问题

本文是对 leetcode 回溯题的一些模板进行整理总结,很多关于回溯的 blog 都会引用对回溯算法的 official definition 和通用的解题步骤,如果是真的想研究这一算法思想,按照这样的方式来完全没有问题。不过个人觉得如果仅仅只是为了应试,那么掌握一些解题的模板会更直接的帮助理解回溯的算法思想。本文将举一些简单的例子来说明这些模板,不采用树来描述,使得对于数据结构不太了解的读者也相对友好。

基本思想:

回溯问题是对多叉树的深度搜索,遇到不满足条件的节点则回退,递归的搜索答案。在递归调用前,尝试一种可能的方案,那么在递归调用的时候,函数的开始,有判断语句,如果这种方案可行,记录下这种方案,并且 return,否则,继续进行尝试,找到满足条件的解以后,回退到之前的选择。

常见模板:

1、无重复元素的全排列问题(或者有重复元素但是不需要去重)

一般在回溯的过程中,不断缩小原来数组的范围并添加至 t r a c k track track 中,直至枚举完所有的元素,满足条件的添加到 r e s u l t result result 数组中, 模板如下

def problem(nums):
	res = []
	def backtrack(nums, track):
		if (判断满足题目所给的条件):  # 如果不限制每个结果都需要用到所有元素,就不需要 if 判断,直接加入 res
			res.append(track[:])   # 这里必须传入track的拷贝,track[:], 否则答案全是空
			return
		for i in range(len(nums)):
			backtrack(nums[:i] + nums[i+1:], track + nums[i])
		backtrack(nums, [])
	return 题目需要的res相关的参数,输出本身,长度,或者其他的

以下题目为实战中套用框架解题

Leetcode 46 全排列

由于是全排列,只要没得选了,那就是我们所需的答案,加入 r e s u l t result result 并且 r e t u r n return return

class Solution:
	def permute(self, nums: List[int]) -> List[List[int]]:
		res = []
		def backtrack(nums, track):
			if not nums:
				res.append(track[:])
				return
			for i in range(len(nums)):
				backtrack(nums[:i] + nums[i+1:], track + [nums[i]])
		backtrack(nums, [])
		return res
2、有重复元素的全排列问题

遇到有重复元素的问题,最好先进行排序,再采用剪枝的方法来进行去重,具体分析见 4。这里给出全排列有重复元素去重的框架:

def problem(nums):
	res = []
	nums.sort()
	def backtrack(nums, track):
		if (判断满足题目所给的条件):  # 如果不限制每个结果都需要用到所有元素,就不需要 if 判断,直接加入 res
			res.append(track[:])   # 这里必须传入track的拷贝,track[:], 否则答案全是空
			return
		for i in range(len(nums)):
			if i > 0 and nums[i] == nums[i-1]:  #剪枝去重
				continue
			backtrack(nums[:i] + nums[i+1:], track + nums[i])
	backtrack(nums, [])
	return 题目需要的res相关的参数,输出本身,长度,或者其他的

Leetcode 1079 活字印刷

先将字符串放在入列表中进行排序,后进行剪枝去重。

由于不需要求具体有哪些排列,因此只需要用一个变量来记录过程中的结果。类似的, N N N皇后与 N N N皇后Ⅱ 的差别也仅在于是否需要建立一个列表或者一个变量来保存结果。

初始 a n s ans ans 设为 -1,因为题目要求最后的结果非空,提前减去一个空字符串。

class Solution:
	def numTilePossibilities(self, tiles: str) -> int:
		self.ans = -1
		tiles = list(tiles)
		tiles.sort()
		def backtrack(tiles):
			self.ans += 1
			for i in range(len(tiles)):
				if i > 0 and tiles[i] == tiles[i-1]:
					continue
				backtrack(tiles[:i] + tiles[i+1:])
		backtrack(tiles)
		return self.ans
3、数组元素不重复且数组元素不可以重复使用的组合问题

这种问题在高中找多少种不同的组合比较常见,比如找 [ 1 , 2 , 3 ] [1,2,3] [1,2,3] 这样的数组有多少种非空的子集,那么我们按照高中的不重复不遗漏的找法,一般是先确定 1 1 1,然后找 2 2 2, 3 3 3 里面的,第一轮找出来是 [ 1 ] [1] [1] , [ 1 , 2 ] [1,2] [1,2] , [ 1 , 3 ] [1,3] [1,3] , [ 1 , 2 , 3 ] [1,2,3] [1,2,3],这时候对于 1 1 1 来说,没有更多的元素可以和它组成子集了,那么现在去掉 1 1 1,再从 [ 2 , 3 ] [2,3] [2,3] 里面找剩余的,第二轮出来的是 [ 2 ] [2] [2], [ 2 , 3 ] [2,3] [2,3],最后一轮从 [ 3 ] [3] [3] 中找,也就是 [ 3 ] [3] [3]。这样我们就得到了不重复不遗漏的所有非空子集。

可以看到,这种问题,越搜索,数据范围越小,比上一轮起始数据向后移动了一位,那么在递归调用中就可以用一个 i n d e x index index 标志 + 1 +1 +1 来表示现在的起始位置从上一轮 + 1 +1 +1 的位置开始。框架如下

def problem(nums):
	res = []
	def backtrack(index, track):
		if (满足题目中的条件):
			res.append(track[:])
				return
		for i in range(index, len(nums)):
			backtrack(i + 1, track + [nums[i]])
	backtrack(0, []) # 这里不一定是0,根据实际的起始条件来给
	return res

以下三题为实战中用框架解题

Leetcode 77 组合

实际问题的返回条件是每个组合内有 k k k 个数,那么就是 t r a c k track track 长度需要是k的时候返回。由于这里题目并没有直接给出数组,是用 1 − n 1-n 1n 来代替,那么起始条件就是 1 1 1,数组用 1 − n 1-n 1n 的范围来代替就好。

class Solution:
	def combine(self, n: int, k: int) -> List[List[int]]:
		res = []
		def backtrack(index, track):
			if len(track) == k:
				res.append(track[:])
				return
			for i in range(index, n+1):
				backtrack(i + 1, track + [i])
		backtrack(1, [])
		return res

Leetcode 78 子集

直接套入框架,这里每一次搜索的路径都要记录下来,那就记录一下每次的路径就行了,不需要再判断什么时候的结果才保存

class Solution:
	def subsets(self, nums: List[int]) -> List[List[int]]:
		res = []
		def backtrack(index, track):
			res.append(track[:])
			for i in range(index, len(nums)):
				backtrack(i + 1, track + [nums[i]])
		backtrack(0, [])
		return res

Leetcode 17 电话号码中的字母组合

此题看上去数组中的数可以重复,比如可以拨打“232”,但是由于是字符串,顺序是一定的,而且拨打第一个 2 2 2 和第二个 2 2 2,对应的字母也可能不同,所以仍然可以看做是数组中元素不重复且不能重复使用的问题。

用字典记录下对应关系,之后代入框架即可,注意读取字典键和值的各种括号就行,最终结果是字符串的时候, t r a c k track track 初始设为“”替代 [ ] [] []

class Solution:
	def letterCombinations(self, digits: str) -> List[str]:
		if not digits:
			return []
		res = []
		dic = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}
		def backtrack(index, track):
			if len(track) == len(digits):
				res.append(track)
				return
			for i in range(len(dic[digits[index]])):
				backtrack(index + 1, track + dic[digits[index]][i])
		backtrack(0, "")
		return res
4、数组元素有重复但不可以重复使用的组合问题

这一类问题和第二种类型的问题相似,最主要的是要对结果进行去重,也就是对深搜的N叉树进行剪枝。比如我们要找 [ 2 , 1 , 2 , 4 ] [2,1,2,4] [2,1,2,4] 有多少种不重复的子集组合,按照我们的高中知识,为了不重复不遗漏,我们应该先排序这个数组,得到 [ 1 , 2 , 2 , 4 ] [1,2,2,4] [1,2,2,4],这时候从1开始找,第一轮是 [ 1 ] [1] [1] , [ 1 , 2 ] [1,2] [1,2],接下来遇到一个相同的 2 2 2,我们为了不重复,会跳过它,不看,因为 l e n = 2 len = 2 len=2 的时候,如果再选 2 2 2,就会得到重复的结果,然后是 [ 1 , 4 ] [1,4] [1,4], [ 1 , 2 , 2 ] [1, 2, 2] [1,2,2], [ 1 , 2 , 4 ] [1, 2, 4] [1,2,4], [ 1 , 2 , 2 , 4 ] [1,2,2,4] [1,2,2,4],我们在找 l e n = 3 len=3 len=3 的时候,同样,当第二位选了第一个 2 2 2 以后,第二位就不再考虑选第二个 2 2 2 的情况,因为它们的结果相同,至此,第一轮结束。

第二轮去掉 1 1 1,在 [ 2 , 2 , 4 ] [2,2,4] [2,2,4] 里面找, [ 2 ] [2] [2], [ 2 , 2 ] [2,2] [2,2], [ 2 , 4 ] [2,4] [2,4], [ 2 , 2 , 4 ] [2,2,4] [2,2,4], 第三轮去掉一个 2 2 2,本来应该在 [ 2 , 4 ] [2,4] [2,4] 里面找,假如我们这样找结果,会得到 [ 2 ] [2] [2], [ 2 , 4 ] [2,4] [2,4],产生重复,因为 [ 2 , 4 ] [2,4] [2,4] 的情况已经包含在 [ 2 , 2 , 4 ] [2,2,4] [2,2,4] 中了,这就是有重复元素的情况下,我们在同一个位置进行选择的时候,应该跳过相同的元素,否则会产生重复。第三轮实际在 [ 4 ] [4] [4] 里面找,得到 [ 4 ] [4] [4]

框架如下

def problem(nums):
	res = []
	nums.sort() # 排序,为了后面去重做准备
	def backtrack(index, track):
		if (满足题目条件):
			res.append(track[:])
		for i in range(index, len(nums)):
		### 进行剪枝,跳过相同位置重复的数字选择
			if i > index and nums[i] == nums[i-1]: 
				continue
			backtrack(i + 1, track + [nums[i]])
	backtrack(0, [])
	return res 

以下两题为实战中用框架解题

Leetcode 90 子集2

搜索路径上所有结果全部保留,直接套入上述框架即可

class Solution:
	def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
		res = []
		nums.sort()
		def backtrack(index, track):
			res.append(track[:])
			for i in range(index, len(nums)):
				if i > index and nums[i] == nums[i-1]:
					continue
				backtrack(i + 1, track + [nums[i]])
		backtrack(0, [])
		return res

Leetcode 40 组合总和2

这里唯一的差别是在于需要把目标和也一起代入进递归调用中,每次判断如果是目标和就加入最终结果,加超过了目标和那就不符合,直接跳出

class Solution:
	def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
		candidates.sort()
		res = []
		def backtrack(index, track, target):
			if target == 0:
				res.append(track[:])
				return
			for i in range(index, len(candidates)):
				if target - candidates[i] < 0: # 超过目标和
					break
				if i > index and candidates[i] == candidates[i-1]:
					continue
				backtrack(i + 1, track + [candidates[i]], target - candidates[i])
		backtrack(0, [], target)
		return res
5、数组元素不重复但可以重复使用

这一类的问题同样也是第二种问题演变而来,唯一的区别是递归调用 b a c k t r a c k backtrack backtrack 的时候,把 i + 1 i + 1 i+1 改成 i i i ,那么下一个位置又可以用这个元素了,即可实现有重复

Leetcode 39 组合总和

class Solution:
	def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
		res = []
		candidates.sort()
		def backtrack(index, track, target):
			if target == 0:
				res.append(track[:])
				return
			for i in range(index, len(candidates)): 
				if target - candidates[i] < 0:
					break
                ### 把原来递归的时候 i+1 改成 i,当前的元素又可以再用一次了
				backtrack(i, track + [candidates[i]], target - candidates[i])
		backtrack(0, [], target)
		return res
文章在cnblogs 同步更新,喜欢的话为我点个赞吧~

https://www.cnblogs.com/HMJIang/p/13575005.html

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页