总结了几类遇到的递归+回溯题。其实总的思路都大差不差,主要是剪枝时需要挨个分析一下。
第一类(求子集)
Leetcode 90. 子集II
题目描述:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
思路
本题总思路与DFS一致,只不过因为求的是子集而不是全排列,所以需要在DFS每深入一步时就将当前的(子集)添加进列表。这里的重点是剪枝条件,题目中的要求是“解集不能包含重复的子集”,以示例1为例的话,假设中的两个'2'分别为'2a'和'2b',即解集中不能同时出现[1, 2a, 2b]和[1, 2b, 2a]这样的子集。
如果将子集的DFS过程想象为树结构的话,就相当于树的同一层级中不能出现重复的数字。这里偷个懒借用一下Leetcode上的高赞题解(代码随想录,附上链接:力扣)中画的图,以便更容易理解树结构。
转变为代码思路的话,也就是同一层的循环中,不能出现重复的数字。那么为了实现去重,需要先将进行排序,排序后,相同的数字必定会挨在一起,所以根据即可判断是否已经出现过了。
Python3代码
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
result = []
nums.sort() # 进行剪枝的话,需要先对nums排序
def dfs(begin, tmp):
result.append(tmp)
for i in range(begin, len(nums)):
if i > begin and nums[i] == nums[i - 1]: # 剪枝:把tmp想象成树结构的话,相当于不允许出现同层级中相同数字重复的情况。
continue
dfs(i + 1, tmp + [nums[i]])
dfs(0, [])
return result
第二类(求全排列)
Leetcode 47. 全排列II
题目描述:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
思路
本题的解题思路和Leetcode 90. 子集II相同,差别只是在进行剪枝时,求子集的判断条件是 if i > begin and nums[i] == nums[i - 1],因为递归的时候下一个startIndex(begin)是i+1而不是0,所以直接 i > startIndex即可。而求全排列时的判断条件是 if i > 0 and nums[i] == nums[i-1] and visited[i-1] == 0。之所以全排列需要使用额外的visited数组判断是因为求全排列时每次要从0开始遍历,所以为了跳过已经入栈(path)的元素,就需要额外一个数组判断当前元素是否已经入栈。而 visited[i-1] == 0,则表示的是剪掉重复数字第二次出现的那条树枝,即剪掉第二个重复元素的递归。比如 nums = [1, 1, 2],进行第一次for循环时,遍历到的第一个1对应的 visited = [1, 0, 0],第二个1对应的 visited = [0, 1, 0],即 visited[i-1] = visited[0] == 0,进行剪枝。
Python3代码
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
result = []
path = []
n = len(nums)
visited = [0] * n # 为1则表示nums[i]已经访问过了
nums.sort() # 对原数组排序,保证相同的数字相邻,以便后面进行剪枝操作
def backtrack(path):
if len(path) == n:
# if path not in result: # 如果不进行剪枝,就需要在添加至result中时去除重复。这样时间会慢超过十倍
result.append(path[:])
return
for i in range(n):
if visited[i]:
continue
# 以下判断,因为是求全排列,每次要从0开始遍历,为了跳过已入栈的元素,需要使用visited来判断是否已经入栈。
# 如果是像求子集,可以不用visited数组来去重,因为递归的时候下一个startIndex是i+1而不是0,所以直接 i > startIndex即可。
if i > 0 and nums[i] == nums[i-1] and visited[i-1] == 0: # 剪枝:解决重复问题,即保证在添加第i个数时,重复数字只会被添加一次
continue
visited[i] = 1
path.append(nums[i])
backtrack(path)
path.pop()
visited[i] = 0 # path回溯了之后,visited[i]也需要进行回溯
backtrack(path)
return result
第三类(求组合)
Leetcode 39. 组合总和
题目描述:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都 互不相同
1 <= target <= 500
思路
本题由于求的是组合总和,所以剪枝条件可以从总和的值上进行思考。因为 1 <= candidates[i] <= 200,所以当当前组合总和大于target,即 target - sum(path) < 0 时,不管之后组合再加进什么数,都不可能使得 target - sum(path) == 0 ,所以这时就可以剪枝了。
Python3代码
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
result = []
path = []
candidates.sort()
def backtrack(begin, path, target):
if not target:
result.append(path[:])
return
for i in range(begin, len(candidates)): # 避免path重复,即同时出现[2, 2, 3]和[2, 3, 2]这样的情况
res = target - candidates[i]
if res < 0: # 剪枝:即如果当前path的元素和已经大于target,则进行剪枝
break
path.append(candidates[i])
backtrack(i, path, res)
path.pop()
backtrack(0, path, target)
return result
Leetcode 40. 组合总和II
题目描述:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[[1,1,6],
[1,2,5],
[1,7],
[2,6]]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[[1,2,2],
[5]]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
Python3代码
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
result = []
path = []
candidates.sort()
def backtrack(begin, path, target):
if not target:
result.append(path[:])
return
for i in range(begin, len(candidates)):
res = target - candidates[i]
if res < 0:
break
if i > begin and candidates[i] == candidates[i - 1]: # 剪枝
continue
path.append(candidates[i])
backtrack(i + 1, path, res)
path.pop()
backtrack(0, path, target)
return result