赞
踩
写在前面的话:感谢liweiwei大佬的解析
回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
「回溯算法」与「深度优先遍历」都有「不撞南墙不回头」的意思。我个人的理解是:「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性
class Solution: def permute(self, nums: List[int]) -> List[List[int]]: res = [] size = len(nums) used = [False for _ in range(size)] def dfs(nums,index,temp,used): if index == len(nums): res.append(temp[:]) return for i in range(len(nums)): # 每一次都遍历整个数组 if not used[i]: # 知道数组中的元素没有被标记为使用,就可以添加到temp中去 used[i] = True temp.append(nums[i]) dfs(nums,index+1,temp,used) used[i]=False temp.pop() dfs(nums,0,[],used) return res
class Solution: def permuteUnique(self, nums: List[int]) -> List[List[int]]: # 回溯+ 深度优先搜索算法 def dfs(nums,size,depth,temp,used): if depth == len(nums): res.append(temp.copy()) # 深拷贝,以及完全拷贝,新的地址,原来的变化,新的不会跟着变化 return for i in range(size): if not used[i]: if i>0 and nums[i] == nums[i-1] and not used[i-1]: continue # 如果前一个没有没用到,那么后一个相同的也不会被使用到 # 目的是避免同一层级出现相同的情况。 # 全排列问题需要有一个used数组类对应记录当前这个值是否被使用过,用来避免重复 used[i] = True temp.append(nums[i]) dfs(nums,size,depth+1,temp,used) temp.pop() used[i] =False size = len(nums) if size == 0: return [] res =[] used = [False for _ in range(size)] nums.sort() dfs(nums,size,0,[],used) return res
这一题最关键的地方就是剪枝的部分,我们可以想象输入为[1,1,2]的情况下的树形图(上传失败的图片)。只有当前在进入到 1-> 1->2,1->2->1 ,且遍历完了这个支线下的所有路径, 从第一位到第二位的1时,因为是排序后的数组,相同元素必在一起,同一个相同元素在DFS回溯算法下只需要第一条路径就会遍历完所有可能的路径,所以这就是为什么( not used[i-1]) 满足就会直接跳过,因为当 nums[0] == nums[1] ==1 的时候,且此时nums[0] 为False,说明是遍历完回退回来的状态,表示相同元素的所有路径已经遍历完了,所以直接跳过nums[1]
if not used[i]:
if i>0 and nums[i] == nums[i-1] and not used[i-1]:
continue
class Solution: def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: def dfs(candidates, begin, size, path, res, target): if target == 0: res.append(path.copy()) return for index in range(begin, size): # 剪枝,target减一个较小的数是负数,那么减一个较大的数也是负数 residue = target - candidates[index] if residue < 0: break dfs(candidates, index, size, path + [candidates[index]], res, residue) # 若是可以重复选择元素,则begin到递归的这一步就不用+1, # +1是保证每一个元素在每一次分支中只可以选择一次,而不加1,保证index和begin相同 # 是可以让同一个元素在多次递归中可以每一次都能选择到这个相同的值 size = len(candidates) if size == 0: return [] candidates.sort() path = [] res = [] dfs(candidates, 0, size, path, res, target) return res
class Solution: def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]: def dfs(size,residue,temp,begin): if residue == 0: res.append(temp.copy()) return for i in range(begin,size): #避免重复元素 if candidates[i] > residue: break # 一旦数组内的值比剩余的值大,直接break当前循环,返回上一层 if i>begin and candidates[i] == candidates[i-1]: continue # 这是在同一层级里面的 两个相同元素的分支,由于遍历元素会遍历 # [2,2,5],所以 此时 candiates[begin] == 2,由于i>begin,所以不会跳过相同的元素, # 而且元素是以及排序好的数组 # 且 candiates[i]若和candiates[i-1]相同,当前两个元素都去的话,就会出现两个[1,2,2,5] # 而只要在这一步跳过相同元素,进入下一层,则前一个相同元素就不会以及添加到了temp中, #这样后一个元素在下一层也会被添加到temp中,避免出现相同的队列的情况 # 这一步的过程是避免了同一层级出现两个相同的元素。 # 由于 [1,2,2,5] 这样子在第二次 temp.append(candidates[i]) dfs(size,residue-candidates[i],temp,i+1) # 这里i+1急速保证组合内的每个值只会在一个分支内使用一次 temp.pop() # 每个数字只能用一次,说明同一个元素不能重复出现 res = [] size = len(candidates) candidates.sort() # 排序数组,从小到大。 dfs(size,target,[],0) return res
什么时候使用 used 数组,什么时候使用 begin 变量。有些朋友可能会疑惑什么时候使用 used 数组,什么时候使用 begin 变量。这里为大家简单总结一下:
class Solution: def combinationSum3(self, k: int, n: int) -> List[List[int]]: #求解 DFS 回溯剪枝 res = [] def dfs(n,k,index,temp): if n==0 and k==0: res.append(temp[:]) #添加整个temp的序列 return for i in range(index,10): if n-i<0 or k<=0: # 此时的k是上一步传来的数,若为0,则表示以及数以及满了,不能再加了 # 而 n-i 判断的是此时的数是否超过了阈值,=0不能作为break的条件。 break # 由于以及 n-i< 0了,那么在其之后减去更大的数 同样也是小于0的 temp.append(i) dfs(n-i,k-1,i+1,temp) temp.pop() # 回溯反弹之后,进入到下一个index,要把原来的index弹出,回溯到原始状态 return dfs(n,k,1,[]) return res
组合问题,其实都是DFS问题, DFS问题一般都离不开递归问题,做这种类似的题的第一步是找到返回条件,这一题(给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。) 明显的看到条件是 len(temp) == k 的时候,返回递归。
class Solution: def combine(self, n: int, k: int) -> List[List[int]]: ##特殊情况处理 if n==0 or k==0: return res def dfs(nums,temp,index): # 首先是判断返回条件 if len(temp) == k: res.append(temp.copy()) return for i in range(index,n): #因为已经是从小到打的排序过程,index又不断再向后,所以不会存在后一个比前一个小 temp.append(nums[i]) dfs(nums,temp,i+1) # 这里的i+1才是真正做到了避免重复的情况,因为必须是将index赋给 i+1 temp.pop() nums = [i for i in range(1, n + 1)] res = [] dfs(nums,[],0) return res # 这一题也类似于使用begin ,这里的index 其实就是begin, 因为这里的排列不可以重复数组,不可以前后顺序调换 # 所以不需要使用used,而且有限定的k值,只要遍历再输出就可以了
子集问题和排列问题有相似之处,只不过需要将每一个可能的非重复组合都要找到,本质上还是DFS, 而且这题也许要使用到begin变量,每次递归begin+1,这样后续遍历会忽略当前的值。但是这一题还是回溯算法,也可以使用迭代算法(后续贴出),这里主要关注回溯,
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
#子集,思路1,DFS 加递归
# 同样判断条件是需要begin 和 k值
visited = []
k = len(nums)
def dfs(begin,temp):
visited.append(temp)
for i in range(begin,k):
dfs(i+1,temp+[nums[i]])
#当bagin == k的时候,当前循环全部结束,返回到最上层的循环。
dfs(0,[])
return visited
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。