回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。——LeetCode
回溯法本质上是一种穷举或者说暴力(brute force),与暴力的不同之处在于,是一种聪明的暴力方法,聪明在于“不能进则退回来”,这减少了遍历的次数。
另外,回溯法可能还通常与递归、DFS(Depth First Search,深度优先搜索)等名词被人提及,实际上这些方法在思想上都是一致的。
对于算法学习,keep your hands dirty
就是捷径,在你没有实际写代码之前,看的再多可能也不可能深刻认识到某些问题。所以多说无益,下面将通过LeetCode上的一系列相关题目来说明回溯算法的思想,题目列表如下:
39. 组合总和(Combination Sum)
40. 组合总和 II(Combination Sum II)
46. 全排列(Permutations)
47. 全排列 II(Permutations II)
78. 子集(Subsets)
90. 子集 II(Subsets II)
class Solution(object):
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
res = []
def dfs(cur_list, cur_target, index):
for i in range(index, len(candidates)):
num = candidates[i]
if num == cur_target:
res.append(cur_list + [num])
elif num < cur_target:
dfs(cur_list + [num], cur_target - num, i)
dfs([], target, 0)
return res
代码并不复杂,代码思路的详细讲解,大家可以移步到LeetCode相应题目下的题解一栏,我并不打算增加互联网的冗余,在这里我只会说一些自己觉得需要注意到的点。
- 题目描述中需要注意的:
candidates
无重复元素;candidates
中数字可取无限次。- 内部函数
dfs()
的使用。dfs()
每一次递归调用,实际上就是一次选路的过程,整个过程和深度优先搜索类似,所以个人喜欢把该函数命名为dfs。这是一个通用的模版,你也可以不使用内部函数,但如果语言支持内部函数,那请尽量使用,这样代码封装性较好。- 函数
dfs()
的index
参数,index
保证了题目中“解集不能包含重复的组合”
这一要求。可以这样理解,对于可选的数字candidates
,如果先选了后一个数就不能再选前一个数了。以示例1
中的输入为例,第一次进入dfs()
时,当前可选数字为[2, 3, 6, 7]
,假设现在遍历到数字3
,下一层递归中index = 1
(数字3
的数组索引为1),此时就不会再选到3
的前一个数2
。
那如果我们先选了后一个数就不再选前一个数,不会漏了一些组合吗?不会的。任选了一个数,这个数之后的所有数,都是可选的,这个数和它之后的所有数的组合都是可以取到的。然而此时,如果你选了后一个数,再去和它的前一个数进行组合,显然就发生了重复。
class Solution(object):
def combinationSum2(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
res = []
candidates.sort()
def dfs(cur_nums, path, cur_target):
left = 0
while left < len(cur_nums):
num = cur_nums[left]
if num == cur_target:
res.append(path + [num])
return
elif num < cur_target:
dfs(cur_nums[left + 1:], path + [num], cur_target - num)
left += 1
while left < len(cur_nums) and cur_nums[left] == cur_nums[left - 1]:
left += 1
else:
break
dfs(candidates, [], target)
return res
- 39题的进化版,与之前不同的是,
candidates
中含有重复的数字;candidates
中每个数字只能使用一次- 这题中我们是怎么处理重复的组合的呢?首先,对
candidates
进行了排序,那么为什么要排序呢,因为candidates
有重复,并且题目没说candidates
是有序的,排序之后就可以把重复的数字聚到一起,而这是我们希望要的效果,可以方便我们之后的操作。
然后,在dfs()
中,这里的思路还是和39题一致,就是先选了后一个数就不能再选前一个数了(这里用了cur_nums
来保存当前递归中,我们可以选择的候选数字,这和上一题的index
作用是一样的,你也可以继续使用index
来限制候选数字)。但仅这样做还是会重复,原因还是candidates
有重复,对于这个问题,我们的做法就是,先把当前位置后移一位left += 1
,然后再比较cur_nums[left]
和cur_nums[left - 1]
(排序把重复数字聚到了一起),如果相等就把left
不断往后移。
class Solution(object):
def permute(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res = []
def dfs(cur_nums, path):
if not cur_nums:
res.append(path)
for (i, num) in enumerate(cur_nums):
dfs(cur_nums[:i] + cur_nums[i + 1:], path + [num])
dfs(nums, [])
return res
nums
没有重复,本题中需要的是排列,对于排列而言,次序也是重要信息,而之前两题是组合,组合不用考虑次序。- 因为次序不同是不同的排列,所以本题不用像之前那样(先选了后一个数就不能再选前一个数了),在这里我们使用
cur_nums
来保存当前候选数字,每次递归时当前候选数字变为了cur_nums[:i] + cur_nums[i + 1:]
,这相当于把当前数字拿掉了。
class Solution(object):
def permuteUnique(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res = []
nums.sort()
def dfs(cur_nums, path):
if not cur_nums:
res.append(path)
left = 0
while left < len(cur_nums):
dfs(cur_nums[:left] + cur_nums[left + 1:], path + [cur_nums[left]])
left += 1
while left < len(cur_nums) and cur_nums[left] == cur_nums[left - 1]:
left += 1
dfs(nums, [])
return res
- 包含重复数字的序列
- 与上一题基本相同,关键在于如何去除重复数字导致的重复排列,具体办法与40题一致,就是先排序,然后遍历的时候,对于被排序到一块的相同的数字,我们只处理第一个。
class Solution(object):
def subsets(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res = []
def dfs(index, path):
res.append(path)
for i in range(index, len(nums)):
dfs(i + 1, path + [nums[i]])
dfs(0, [])
return res
- 这里要解的是子集,并且
nums
中不含有重复数字,- 如果上面的题你都能做出来,那这题实际上也一样,唯一的区别就是每次递归都要
res.append(path)
,因为求得是子集,结果中不重复的保证靠的还是那句话“先选了后一个数就不能再选前一个数了”,具体到代码中,是通过index
来达到这个要求的。
class Solution(object):
def subsetsWithDup(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res = []
nums.sort()
def dfs(cur_set, index):
res.append(cur_set)
left = index
while left < len(nums):
dfs(cur_set + [nums[left]], left + 1)
left += 1
while left < len(nums) and nums[left] == nums[left - 1]:
left += 1
dfs([], 0)
return res
- 上一题的含重复数字版本,关键是在上一题的基础上处理重复数字的问题
- 重复数字的的解决办法,和之前的题目也如出一辙,还是老规矩,先排序,然后只处理重复数字中的第一个