本文介绍 LeetCode 题集中,使用回溯法(递归)解决数组相关的问题。
LeetCode 其他有关回溯法的问题:
LeetCode 题集:回溯法和递归(二)字符串相关问题
LeetCode 题集:回溯法和递归(三)矩阵相关问题
46. Permutations(全排列)
问题描述
思路与代码
本题的思路为,基于回溯法的思想,通过不断交换两个位置的数字,完成对所有可能排列的遍历,存储并输出结果。
代码如下:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def swap(list_num: List[int], i: int, j: int):
list_num[i], tmp = list_num[j], list_num[i]
list_num[j] = tmp
def dfs(list_num: List[int], pos: int, list_pmt: List[List[int]]):
"""
深度优先搜索的递归函数
:param list_num: 当前数组
:param pos: 当前交换点的位置
:param list_pmt: 结果列表
:return: 无
"""
if pos == len(list_num) - 1:
list_pmt.append(list_num.copy()) # 注意 copy
return
for i in range(pos, len(list_num)):
swap(list_num=list_num, i=pos, j=i) # 交换两个位置的数字
dfs(list_num=list_num, pos=pos + 1, list_pmt=list_pmt) # 交换点的位置右移一位
swap(list_num=list_num, i=pos, j=i) # 把两个位置的数字交换回来
list_pmt_ = []
dfs(list_num=nums, pos=0, list_pmt=list_pmt_)
return list_pmt_
运行效果:
47. Permutations II(全排列 II)
问题描述
思路与代码
本题为前一题的变体,不同之处在于数组中可能有重复元素,但全排列的结果中不能存在重复,处理掉该情况即可解决问题。
具体代码如下:
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
def swap(list_num: List[int], i: int, j: int):
list_num[i], tmp = list_num[j], list_num[i]
list_num[j] = tmp
def dfs(list_num: List[int], pos: int, list_pmt: List[List[int]]):
"""
深度优先搜索的递归函数
:param list_num: 当前数组
:param pos: 当前交换点的位置
:param list_pmt: 结果列表
:return: 无
"""
if pos == len(list_num) - 1:
list_pmt.append(list_num.copy()) # 注意 copy
return
for i in range(pos, len(list_num)):
if_swap = True
for j in range(pos, i):
if list_num[j] == list_num[i]: # 若遍历过的位置中出现过当前位置的数字,则不交换
if_swap = False
break
if if_swap:
swap(list_num=list_num, i=pos, j=i) # 交换两个位置的数字
dfs(list_num=list_num, pos=pos + 1, list_pmt=list_pmt) # 交换点的位置右移一位
swap(list_num=list_num, i=pos, j=i) # 把两个位置的数字交换回来
list_pmt_ = []
dfs(list_num=nums, pos=0, list_pmt=list_pmt_)
return list_pmt_
运行效果:
39. Combination Sum(组合总和)
问题描述
思路与代码
本题的思路,将数组降序排列,然后从每个位置进行递归,对于以每个位置为起点的组合,只能从当前位置及其后面的数字中寻找,及确保组合的不重复性。
具体代码如下:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrack(list_cdd: List[int], tgt: int, pos: int, group: List[int], list_group: List[List[int]]):
if not tgt:
list_group.append(group.copy())
return
for i in range(pos, len(list_cdd)):
if list_cdd[i] > tgt:
continue
group.append(list_cdd[i])
backtrack(list_cdd=list_cdd, tgt=tgt - list_cdd[i], pos=i,
group=group, list_group=list_group) # 更新 target;以当前位置 i 为起始位置 pos 进行递归
group.pop()
list_cdd_ = sorted(candidates, reverse=True) # 数组降序排列
list_group_ = []
backtrack(list_cdd=list_cdd_, tgt=target, pos=0, group=[], list_group=list_group_)
return list_group_
运行效果:
40. Combination Sum II(组合总和 II)
问题描述
思路与代码
本题为前一题的变体,不同之处在于备选数组中存在重复元素,而在结果中,重复元素出现的次数不能超过备选数组中元素的重复次数。
代码直接在前一题的基础上进行修改即可,改动有两处,其一,在同一起始位置的循环过程中,跳过与前一位置数值相同的位置;其二,下次递归从当前位置的下一个位置开始,以保证当前位置的数字不被重复使用。
具体代码如下:
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrack(list_cdd: List[int], tgt: int, pos: int, group: List[int], list_group: List[List[int]]):
if not tgt:
list_group.append(group.copy())
return
for i in range(pos, len(list_cdd)):
if list_cdd[i] > tgt:
continue
if i > pos and list_cdd[i] == list_cdd[i - 1]: # 若所在位置与同次循环的前一位置数值相同,则跳过
continue
group.append(list_cdd[i])
backtrack(list_cdd=list_cdd, tgt=tgt - list_cdd[i], pos=i + 1,
group=group, list_group=list_group) # 更新 target;以下一个位置 i + 1 为起始位置 pos 进行递归
group.pop()
list_cdd_ = sorted(candidates, reverse=True) # 数组降序排列
list_group_ = []
backtrack(list_cdd=list_cdd_, tgt=target, pos=0, group=[], list_group=list_group_)
return list_group_
运行效果:
77. Combinations(组合)
问题描述
思路与代码
有了前面几题解决排列组合问题的经验,解决本题会相对简单(因为只是做一个简单的组合枚举),代码如下:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def backtrack(list_num: List[int], m: int, pos: int, group: List[int], list_group: List[List[int]]):
if not m:
list_group.append(group.copy())
return
for i in range(pos, len(list_num)):
group.append(list_num[i])
backtrack(list_num=list_num, m=m - 1, pos=i + 1, group=group, list_group=list_group)
group.pop()
list_num_ = list(range(1, n + 1))
list_group_ = []
backtrack(list_num=list_num_, m=k, pos=0, group=[], list_group=list_group_)
return list_group_
但运行时发现,效率并不高:
由于本题是单纯的组合枚举,因此其实可以通过非递归的循环来实现,思路类比进位制,具体代码如下:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
group = [i for i in range(1, k + 1)]
list_group = [group.copy()]
while True:
for i in range(k - 1, -1, -1): # 从最末一位向前迭代,类似进位制
if group[i] < n - (k - 1) + i: # 当前位置的最大数字
group[i] += 1 # 当前位置的数字 +1
for j in range(i + 1, k):
group[j] = group[j - 1] + 1 # 当前位置后面的数字,在前一位的基础上 +1
list_group.append(group.copy())
break # 彻底跳出 for 循环,下次循环从头开始
elif i == 0: # return 语句不放在外面的原因:无法跳出 while 循环
return list_group
运行效果:
78. Subsets(子集)
问题描述
思路与代码
本题的思路有两种:
其一, 延续前几题的通用思路,使用回溯法进行遍历。
可以和前面的思路一样,按位遍历:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
subset = []
list_subset = []
def backtrack(pos: int):
list_subset.append(subset.copy())
for i in range(pos, len(nums)):
subset.append(nums[i])
backtrack(pos=i + 1)
subset.pop()
backtrack(pos=0)
return list_subset
运行效果:
也可以类比进位制,根据每个位置的数字是否选取进行遍历:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
subset = []
list_subset = []
def backtrack(pos: int):
if pos == len(nums):
list_subset.append(subset.copy())
return
for b in [False, True]:
if b:
subset.append(nums[pos])
backtrack(pos=pos + 1)
if b:
subset.pop()
backtrack(pos=0)
return list_subset
运行效果:
其二, 由于子集的数量为 2 n 2^n 2n,因此可以使用二进制数进行循环遍历。
代码如下:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
list_subset = []
n = len(nums)
s = 2 ** n # 子集数量
for i in range(s):
list_bit = list(bin(i)[2:]) # 转为二进制数
for j in range(n - len(list_bit)): # 补齐二进制数
list_bit = ['0'] + list_bit
subset = []
for j in range(len(list_bit)):
if list_bit[j] == '1':
subset.append(nums[j])
list_subset.append(subset.copy())
return list_subset
运行效果:
90. Subsets II(子集 II)
问题描述
思路与代码
本题为前一题的变体,不同之处在于重复元素,处理方法可以参考前面 LeetCode 40 的做法,即:
- 将输入的数组排序
- 在同一起始位置的循环过程中,跳过与前一位置数值相同的位置
具体代码如下:
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort() # 数组排序
subset = []
list_subset = []
def backtrack(pos: int):
list_subset.append(subset.copy())
for i in range(pos, len(nums)):
if i > pos and nums[i] == nums[i - 1]: # 若所在位置与同次循环的前一位置数值相同,则跳过
continue
subset.append(nums[i])
backtrack(pos=i + 1)
subset.pop()
backtrack(pos=0)
return list_subset
运行效果: