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

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

基本思想:

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

常见模板:

一、全排列问题

通常在回溯的过程当中,不断缩小原来数组的范围并添加至track中,直至枚举完全部的元素,知足条件的添加到result数组中, 模板以下算法

 1 def problem(nums):
 2     res = []
 3     def backtrack(nums, track):
 4         if (判断知足题目所给的条件):
 5             res.append(track[:]) #这里必须传入track的拷贝,track[:], 不然答案全是空
 6             return
 7         for i in range(len(nums)):
 8             backtrack(nums[:i] + nums[i+1:], track + nums[i])
 9     backtrack(nums, [])
10     return 题目须要的res相关的参数,输出自己,长度,或者其余的

 

如下两题为实战中套用框架解题数组

Leetcode 46  全排列数据结构

因为是全排列,只要没得选了,那就是咱们所需的答案,加入result而且returnapp

 1 class Solution:
 2     def permute(self, nums: List[int]) -> List[List[int]]:
 3         res = []
 4         def backtrack(nums, track):
 5             if not nums:
 6                 res.append(track[:])
 7                 return
 8             for i in range(len(nums)):
 9                 backtrack(nums[:i] + nums[i+1:], track+[nums[i]])
10         backtrack(nums, [])
11         return res

 

Leetcode 1079  活字印刷框架

依旧是一个全排列的问题,差异仅仅在于,此次没有限制须要全部的字符都要用到,而是任意长度符合条件都可。惟一的问题在于须要去掉重复的排列,直接使用集合判断是否有重复会很方便,相比于在res数组中用if xxx not in 要有显著的效率的提升。函数

最终结果须要去掉一个空的排列,由于题目要求最后的结果非空。spa

1 class Solution:
2     def numTilePossibilities(self, tiles: str) -> int:
3         res = set()  #使用集合去重
4         def backtrack(tiles, track):
5             res.add(track)
6             for i in range(len(tiles)):
7                 backtrack(tiles[:i] + tiles[i+1:], track + tiles[i])
8         backtrack(tiles, "")
9         return len(res) - 1

二、数组元素重复且数组元素能够重复使用的组合问题

这种问题在高中找多少种不一样的组合比较常见,好比找 [1,2,3] 这样的数组有多少种非空的子集,那么咱们按照高中的不重复不遗漏的找法,通常是先肯定1,而后找2,3里面的,第一轮找出来是 [1], [1,2], [1,3], [1,2,3],这时候对于1来讲,没有更多的元素能够和它组成子集了,那么如今去掉1,再从 [2,3]里面找剩余的,第二轮出来的是 [2], [2,3],最后一轮从 [3] 中找,也就是 [3]。这样咱们就获得了不重复不遗漏的全部非空子集。code

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

 1 def problem(nums):
 2     res = []
 3     def backtrack(index, track):
 4         if (知足题目中的条件):
 5             res.append(track[:])
 6             return
 7         for i in range(index, len(nums)):
 8             backtrack(i + 1, track + [nums[i]])
 9     backtrack(0, []) #这里不必定是0,根据实际的起始条件来给
10     return res

 

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

Leetcode 77  组合

实际问题的返回条件是每一个组合内有k个数,那么就是track长度须要是k的时候返回。因为这里题目并无直接给出数组,是用1-n来代替,那么起始条件就是1,数组用1-n的范围来代替就好。

 1 class Solution:
 2     def combine(self, n: int, k: int) -> List[List[int]]:
 3         res = []
 4         def backtrack(index, track):
 5             if len(track) == k:
 6                 res.append(track[:])
 7                 return
 8             for i in range(index, n+1):
 9                 backtrack(i + 1, track + [i])
10         backtrack(1, [])
11         return res

 

Leetcode 78  子集

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

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

 

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

此题看上去数组中的数能够重复,好比能够拨打“232”,可是因为是字符串,顺序是必定的,并且拨打第一个2和第二个2,对应的字母也可能不一样,因此仍然能够看作是数组中元素不重复且不能重复使用的问题。

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

 1 class Solution:
 2     def letterCombinations(self, digits: str) -> List[str]:
 3         if not digits:
 4             return []
 5         res = []
 6         dic = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}
 7         def backtrack(index, track):
 8             if len(track) == len(digits):
 9                 res.append(track)
10                 return
11             for i in range(len(dic[digits[index]])):
12                 backtrack(index + 1, track + dic[digits[index]][i])
13         backtrack(0, "")
14         return res

三、数组元素重复但能够重复使用的组合问题

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

第二轮去掉1,在[2,2,4]里面找,[2],  [2,2], [2,4], [2,2,4], 第三轮去掉一个2,原本应该在[2,4]里面找,假如咱们这样找结果,会获得 [2], [2,4],产生重复,由于 [2,4] 的状况已经包含在 [2,2,4] 中了,这就是有重复元素的状况下,咱们在同一个位置进行选择的时候,应该跳过相同的元素,不然会产生重复。第三轮实际在 [4] 里面找,获得 [4]。

框架以下

 1 def problem(nums):
 2     res = []
 3     nums.sort() #排序,为了后面去重作准备
 4     def backtrack(index, track):
 5         if (知足题目条件):
 6             res.append(track[:])
 7             for i in range(index, len(nums)):
 8                 ###进行剪枝,跳过相同位置重复的数字选择
 9                 if i > index and nums[i] == nums[i-1]: 
10                     continue
11                 backtrack(i + 1, track + [nums[i]])
12     backtrack(0, [])
13 return res 

 

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

Leetcode 90  子集2

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

 1 class Solution:
 2     def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
 3         res = []
 4         nums.sort()
 5         def backtrack(index, track):
 6             res.append(track[:])
 7             for i in range(index, len(nums)):
 8                 if i > index and nums[i] == nums[i-1]:
 9                     continue
10                 backtrack(i + 1, track + [nums[i]])
11         backtrack(0, [])
12         return res

 

Leetcode 40  组合总和2

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

 1 class Solution:
 2     def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
 3         candidates.sort()
 4         res = []
 5         def backtrack(index, track, target):
 6             if target == 0:
 7                 res.append(track[:])
 8                 return
 9             for i in range(index, len(candidates)):
10                 if target - candidates[i] < 0: # 超过目标和
11                     break
12                 if i > index and candidates[i] == candidates[i-1]:
13                     continue
14                 backtrack(i + 1, track + [candidates[i]], target - candidates[i])
15         backtrack(0, [], target)
16         return res

四、数组元素重复但能够重复使用

这一类的问题一样也是第二种问题演变而来,惟一的区别是递归调用backtrack的时候,把 i + 1 改为 i ,那么下一个位置又能够用这个元素了,便可实现有重复

Leetcode 39  组合总和

 1 class Solution:
 2     def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
 3         res = []
 4         candidates.sort()
 5         def backtrack(index, track, target):
 6             if target == 0:
 7                 res.append(track[:])
 8                 return
 9             for i in range(index, len(candidates)): 
10                 if target - candidates[i] < 0:
11                     break
12                 ###把原来递归的时候 i+1 改为 i,当前的元素又能够再用一次了
13                 backtrack(i, track + [candidates[i]], target - candidates[i])
14         backtrack(0, [], target)
15         return res
相关文章
相关标签/搜索