leetcode——递归,回溯
回溯递归是暴利穷举的一种变体,很多复杂的问题,通过递归回溯的方法都可以寻找到最终解,缺点是时间复杂度比较高。为了减少算法实际运行的时间,可以通过剪枝操作来限制算法是否继续递归,合理的剪枝可以让指数级复杂度的算法,也能在相对较短的时间内完成搜索。因此递归回溯法,较为关键的一点是怎么设计剪枝。
现在总结一下在leetcode刷题过程中遇到的几个关于递归回溯的问题,以便日后查找翻阅,若有错误与不足之处,期待您给与相应的建议。
1、列表元素的全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解题思路:
以题目的给定的输入来说,输出所有可能的全排列,在每一个位置i分别有n中可能性,n表示了没有重复输入的个数,最简单的,通过穷举的方式,可以得到[1,1,1],[1,1,2]…,[3,3,3]这样27种可能的结果,但是其中包含多个不符合要求的排列,1.拥有重复元素的不符合要求; 所以就需要将这些结果给过滤掉。
这里采用的方法是递归回溯
class Solution:
def permute(self, nums):
answer =[]
result =[]
length =len(nums)
idx=0
self.DFS(nums,answer,result,s,idx,length)
return result
def DFS(self,nums,answer,result,s,idx,length):
if idx==length :
s.append(answer[:])
if len(list(set(answer)))==length:
result.append(answer[:])
return
elif idx<length and len(answer)>0:
if answer[-1] in answer[:-1]:
return
for i in range(length):
#先加进来,再pop出去
answer.append(nums[i])
self.DFS(nums,answer,result,s,idx+1,length)
answer.pop()
if __name__ == '__main__':
example =Solution()
nums =[1,2,3]
res=example.permute(nums)
print('final :{}'.format(res))
这里,使用了深度优先搜索的方法,遍历所有可能的元素,假设当前的结果没有达到指定数目n,那么将当前的元素存储起来,并递归调用自身,这里会有两个抉择,第一,假设组合结果已经达到n,表示已经抵达树的深处,可以得到[1,1,2],[2,1,3]这样的结果,但是这样的结果并不都是有效的,拥有重复元素的列表是无效的,这里使用set去重,去重后的元素个数依旧是n个,那么表明这个结果是有效的,存储到最终的返回值中去。
上面的做法,虽然用到了递归,实质上还是在穷举,实际情况中,我们可以发现,当穷举出[1,1,…],[2,2,…]这样的例子时,显而易见的是,不管后面的元素是什么,当前的这个sample都符合要求,此时,就可以停止,并回溯到上一个节点了,这样就可以减少递归的次数,这也就是剪枝。所以,当索引号小于n的时候,我们判别当前sample的结果answer中最后一个元素是否与与前面的元素有重复,假设有重复,直接return回去,并使用pop操作,回溯到上一个节点。
这里有一个问题,每次在result后面append answer的时候必须使用answer[:],而不能使用answer(返回的是空,可能后面的pop操作将其中的元素弹出了),没有理解。
括号生成
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
3.组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
思路:
使用深度优先搜索加上剪枝解决问题,首先,需要对未排序的列表进行排序。方便后面的剪枝操作。每次从列表中选取一个元素,由于可以重复选取元素,递归的下一个元素仍是从前元素开始,向后依次遍历,并回溯。
class Solution(object):
'''
回溯算法实际上一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,
当发现已不满足求解条件时,就“回溯”返回(也就是递归返回)
回溯法说白了就是穷举法。回溯法一般用递归来解决。
如果不符合条件,就要回溯,其实回溯也就是递归调用的返回。
----------------------------------------------
很重要的一点是把每次递归的不同信息传递给递归调用的函数
------------------------------------------------
而这里最重要的要传递给递归调用函数的信息,就是把上一步做过的某些事情的这个选择排除,避免重复和无限递归。
***要保证递归函数返回后,状态可以恢复到递归前,以此达到真正回溯。
'''
def __init__(self):
self.a =0
def combinationSum(self, candidates, target):
answer =[]
result =[]
candidates.sort()
length =len(candidates)
self.DFS(candidates,target,0,length,answer,result)
print('a :{}'.format(self.a))
return result
def DFS(self,candidates,target,start,length ,answer,result):
self.a+=1
for i in range(start,length):
if target>0:
#target 大于0 继续调用
answer.append(candidates[i])
self.DFS(candidates,target-candidates[i],i,length,answer,result)
answer.pop()
if target-candidates[i]<=0:
break
elif target<0:
#target 小于 0说明不符合要求,返回空
return
else:
#target =0 符合要求,将当前的结果加在返回值上,并返回空,
#假设不返回,会递归的回到
result.append(answer[:]) #仅仅是answer,值会pop出去
return
组合总和的变体
不可以重复选择元素,candidates里面可以拥有重复的元素。面对这种情况我们只需要对上面的程序稍微的修改即可,递归调用的idx变为下一个数值即idx=i+1,同时为了解决最后一位出现符合要求而不能正确的加载到最后的结果中的情况,我们需要额外的进行判断。
class Solution:
def combinationSum2(self, candidates, target):
"""
candidates中元素只能用一次,同时可能存在同样的元素
使用DFS解决
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
answer=[]
result =[]
candidates.sort()
idx =0
length =len(candidates)
self.DFS(target,candidates,answer,result,idx,length)
return result
def DFS(self,target,candidates,answer,result,idx,length):
#最后一位符合要求的情况
if idx==length and target==0:
#去重
if answer[:] not in result:
result.append(answer[:])
return
for i in range(idx,length):
if target==0 :
#去重
if answer[:] not in result:
result.append(answer[:])
return
elif target<0:
return
else:
answer.append(candidates[i])
self.DFS(target-candidates[i],candidates,answer,result,i+1,length)
answer.pop()
if target-candidates[i]<=0:
break