回溯算法是一种类似于枚举的深度优先搜索过程,其解空间可以表示成一颗搜索树,叶子节点即为可能满足条件的解,树的一个路径就代表搜索其中一个解的方式。
此类问题一般都需要编写一个backtracking函数,用于递归地进行解空间的搜索和判断。 不满足条件时则撤销此次操作,回溯到上一搜索节点。
通常至少需要维护两个变量:res保存当前已经找到的所有满足条件的解、temp保存当前正在搜索的解。
temp自始至终只有一个实例,在回溯时(即对temp执行pop时)由于所有temp指向同一内存,会导致res中的结果也都被修改。
有两种方式解决,一种是利用append和pop,以及浅拷贝(在python中用[:]或copy.copy()来进行浅拷贝)例如下面的前几个题;
第二种是用加号运算符来产生临时的变量传递给backtracking函数,此时就不用在调用函数前append、调用函数后pop了,例如下面的后几个题。
例题:(python解法)
leetcode39 组合总和 https://leetcode-cn.com/problems/combination-sum/
从一个集合中有放回的抽取。
class Solution:
def backtracking(self, res, temp, nums, idx, left):
# left表示距离给定的target还差多少
# 考虑往temp中放入nums的第idx个数后凑成和为target
if left < 0:
return
if left==0:
res.append(temp[:])
return
for i in range(idx, len(nums)):
temp.append(nums[i]) #尝试放入该数
self.backtracking(res, temp, nums, i, left - nums[i]) #递归
temp.pop() #弹出该数,回溯到上一个搜索节点
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort() #先排序,提高执行速度
res = []
self.backtracking(res, [], candidates, 0, target)
return res
leetcode40 组合总和II https://leetcode-cn.com/problems/combination-sum-ii/
从一个集合中无放回的抽取。 由于是无放回的所以要判断重复元素的情况。
class Solution:
def backtracking(self, res, temp, nums, idx, left):
# left表示距离给定的target还差多少
# 考虑往temp中放入nums的第idx个数后凑成和为target
if left < 0:
return
if left==0:
res.append(temp[:])
return
for i in range(idx, len(nums)):
if i>idx and nums[i]==nums[i-1]:
continue # 由于排过序,当碰到重复元素时直接跳过即可
temp.append(nums[i])
self.backtracking(res, temp, nums, i+1, left - nums[i])
temp.pop()
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort() #必须排序
res = []
self.backtracking(res, [], candidates, 0, target)
return res
leetcode46 全排列 https://leetcode-cn.com/problems/permutations/
从一个集合中无放回的抽取。 由于是无放回的所以要判断重复元素的情况。借助一个used数组。
class Solution:
def backtracking(self, res, temp, nums, used):
if len(temp)==len(nums):
res.append(temp[:])
return
for i in range(len(nums)): #遍历集合中所有数字
if used[i]: #如果这个数已经用过了就跳过
continue
temp.append(nums[i]) #没用过,则加入排列
used[i]=True #置该数已被使用
self.backtracking(res, temp, nums, used) #递归搜索
temp.pop() #弹出该数
used[i]=False #置该数未被使用
def permute(self, nums: List[int]) -> List[List[int]]:
res=[]
used=[False for i in range(len(nums))] #给每个数字一个标志位标明该数是否已经使用
self.backtracking(res,[],nums,used)
return res
leetcode17 电话号码的字母组合 https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/
这是一个从多个集合中分别抽取,然后组合起来的题型。
class Solution:
def __init__(self):
self.dic = {"2": "abc", "3": "def",
"4": "ghi", "5": "jkl", "6": "mno",
"7": "pqrs", "8": "tuv", "9": "wxyz"}
def backtracking(self, res, temp, digits, index):
# 尝试给temp添加digits的第index位
if len(temp) == len(digits): #当前解与输入的数字等长时说明搜索结束
res.append(temp) #将该解保存下来
return
for i in range(index, len(digits)): #组合之后几位数字所对应的字母
for char in self.dic[digits[i]]:
self.backtracking(res, temp + char, digits, i + 1) #递归搜索
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
res = []
self.backtracking(res, '', digits, 0)
return res
leetcode22 括号生成 https://leetcode-cn.com/problems/generate-parentheses/
从一个给定集合中无放回的抽取
class Solution:
def backtracking(self, res, temp, n, n1, n2):
# n是括号的对数
# n1/n2分别是已经放置的左/右括号的数量
if n1>n or n2>n:
return # 已经放置的括号数量不可能大于给的括号数量
if n1==n and n2==n:
res.append(temp) # 相等时说明所有括号都放完了,则保存该解
if n1>=n2: # 只有当结果中左括号多于右括号时才能继续放置,否则不满足题目要求的“有效”这个条件
self.backtracking(res, temp+'(', n, n1+1, n2) #先放左括号
self.backtracking(res, temp+')', n, n1, n2+1) #再放右括号
def generateParenthesis(self, n: int) -> List[str]:
res=[]
self.backtracking(res, '', n, 0, 0)
return res