回溯
回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案;
- 在尝试了所有可能的分步方法后宣告该问题没有答案
深度优先搜索dfs
是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支
当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
回溯和DFS都有不撞南墙不回头的意思,
● 回溯更强调了深度的思想,用一个不断变化的变量去尝试各种可能,并且强调了回退对于搜索的合理性
● DFS则是一种遍历的思想
搜索问题的解,可以通过 遍历 实现。所以很多教程把「回溯算法」称为爆搜(暴力解法)。因此回溯算法用于 搜索一个问题的所有的解 ,通过深度优先遍历的思想实现。
与DP的不同
● 都是将求解问题分成很多的阶段,每个阶段有不同的选择
● 不同是,DP只能求导最优解是多少,而具体的最优解是什么并不要求
● 回溯可以获得所有的方案,是一种遍历算法,时间复杂度很高
回溯套路:
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
路径.add(选择)
backtrack(路径, 选择列表)
路径.remove(选择)
撤销选择
三种套路:
● 元素无重不可复选,子集,组合,每种元素最多使用一次
● 元素可重不可复选,nums中有重复,每个元素最多只能被使用一次,关键在于排序和剪枝
● 元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次,只要删掉去重逻辑即可
1.子集-leetcode78
题目给你输入一个无重复元素的数组 nums,其中每个元素最多使用一次,请你返回 nums 的所有子集。
路径:选择数字加入集合
选择列表:选择不同的数字加入集合
结束条件:每一个节点都是子集,每一步都可以加入res
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res=[]
def backtrack(tmp,first):
# 每个节点都是解,直接放
res.append(tmp[:])
for i in range(first, len(nums)):
# 做选择
tmp.append(nums[i])
# 继续递归对下一个数做选择,因为不能选一样的数所以是i
backtrack(tmp,i + 1)
# 撤销操作
tmp.pop()
backtrack([],0)
return res
1.子集:有重复不可复选-leetcode90
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。此时的改动点,就是先排序,每个元素 nums[i] 添加到 path 之前,判断一下 nums[i] 是否等于 nums[i - 1] ,如果相等就不添加到 path 中
重复不可复选,考虑排序剪枝
路径:选择数字加入集合
选择列表:选择不同的数字加入集合,如果之前已经延伸的数和他一样则剪枝(排序前提)
结束条件:每一个节点都是子集,每一步都可以加入res
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
res=[]
def backtrack(tmp,first):
# 每个节点都是解,直接放
res.append(tmp[:])
for i in range(first, len(nums)):
# 做选择,如果在此延伸两次选的数字一样,那么这个选择剪掉(前提是排序了)
if i > first and nums[i] == nums[i - 1]:
continue
tmp.append(nums[i])
# 继续递归对下一个数做选择,因为不能选一样的数所以是i
backtrack(tmp,i + 1)
# 撤销操作
tmp.pop()
nums.sort()#需要排序
backtrack([],0)
return res
2.组合:无重复,不可复选-leetcode77
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
路径:选择数字加入组合
选择列表:选择不同的数字加入组合
结束条件:组合长度为k则加入答案
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res=[]
def backtrack(tmp,first,n,k):
# 遍历到了k层,当前组合的长度为k
if k==len(tmp):
res.append(tmp[:])
if first <= n- k + len(tmp) + 1:
#如果后面的数凑不够k长度直接不用递归了
for i in range(first, n+1):
# 做选择
tmp.append(i)
# 继续递归对下一个数做选择,因为不能选一样的数所以是i
backtrack(tmp,i + 1,n,k)
# 撤销操作
tmp.pop()
backtrack([],1,n,k)
return res
2.组合:无重复可复选-leetcode39
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
考虑删掉去重逻辑
路径:选择数字加入组合
选择列表:可以在选择过的数中选择,但是每次选择要target减去已经选择的数
结束条件:当前target为0,找到答案,target小于0则停止搜索死路
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res=[]
n=len(candidates)
def backtrack(tmp,first,t):
# 当前target<0,没找到解,死路
if t<0:
return
#找到解了,加入res
if t==0:
res.append(tmp[:])
for i in range(first,n):
# 做选择
tmp.append(candidates[i])
# 继续递归自己做选择
backtrack(tmp,i,t-candidates[i])
# 撤销操作
tmp.pop()
backtrack([],0,target)
return res
2.组合:有重复不可复选-leetcode40
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次
有重复,考虑排序剪枝
路径:选择数字加入组合
选择列表:可以在选择过的数中选择,但是每次选择要target减去已经选择的数,如果要选的数和前一个一样则剪枝
结束条件:当前target为0,找到答案,target小于0则停止搜索死路
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
res=[]
n=len(candidates)
def backtrack(tmp,first,t):
# 当前target<0,没找到解,死路
if t<0:
return
#找到解了,加入res
if t==0:
res.append(tmp[:])
for i in range(first,n):
if i > first and candidates[i] == candidates[i - 1]:
continue
# 做选择
tmp.append(candidates[i])
# 继续递归自己做选择
backtrack(tmp,i+1,t-candidates[i])
# 撤销操作
tmp.pop()
candidates.sort()
backtrack([],0,target)
return res
2.组合0-9选,不重复,不可复选-leetcode216
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
分析:从1-9的9个数中选
无重复,不可复选,常规组合,不够k长度剪枝
路径:选择数字加入组合
选择列表:不可以在选择过的数中选择,但是每次选择要n减去已经选择的数i
结束条件:当前target为0并且长度为K的时候,找到答案,target小于0则停止搜索死路,或者后面的数凑不够k长度也直接停止。
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
res=[]
def backtrack(tmp,first,n,k):
# 遍历到了k层,当前组合的长度为k
if n<0:
return
if n==0 and k==len(tmp):
res.append(tmp[:])
if first <= 9- k + len(tmp) + 1:
#如果后面的数凑不够k长度直接不用递归了
for i in range(first, 10):
# 做选择
tmp.append(i)
# 继续递归对下一个数做选择,因为不能选一样的数所以是i+1
backtrack(tmp,i + 1,n-i,k)
# 撤销操作
tmp.pop()
backtrack([],1,n,k)
return res
3.全排列:不重复不可复选-leetcode46
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
路径:将哪个数加入排列
选择列表:在未选择过的数中选择(维护标记数组)
结束条件:排列达到目标len,加入res
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
def backtrack(tmp):
# 所有数都填完了
if len(tmp) == n:
res.append(tmp[:])
for i in range(n):
if used[i]==1:
continue
# 做选择
used[i]=1
tmp.append(nums[i])
# 继续递归填下一个数
backtrack(tmp)
# 撤销操作
tmp.pop()
#此处只能用pop恢复状态
used[i]=0
used=[0 for _ in range(len(nums))]
n = len(nums)
res = []
backtrack([])
return res
#无辅助空间版本
class Solution:
def permute(self, nums):
def backtrack(first = 0):
# 所有数都填完了
if first == n:
res.append(nums[:])
for i in range(first, n):
# 动态维护数组
nums[first], nums[i] = nums[i], nums[first]
# 继续递归填下一个数
backtrack(first + 1)
# 撤销操作
nums[first], nums[i] = nums[i], nums[first]
n = len(nums)
res = []
backtrack()
return res
3.全排列:重复不可复选-leetcode47
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
路径:将哪个数加入排列
选择列表:在未选择过的数中选择(维护标记数组),并且排序后前一个数和现在相等,前一个数没用过即剪枝
结束条件:排列达到目标len,加入res
有重复不可复选考虑排序剪枝
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
def backtrack(tmp):
# 所有数都填完了
if len(tmp) == n:
res.append(tmp[:])
for i in range(n):
if used[i]==1:
continue
if i > 0 and nums[i] == nums[i-1] and used[i-1]!=1:
continue
# 做选择
used[i]=1
tmp.append(nums[i])
# 继续递归填下一个数
backtrack(tmp)
# 撤销操作
tmp.pop()
used[i]=0
used=[0 for _ in range(len(nums))]
n = len(nums)
res = []
nums.sort()
backtrack([])
return res