回溯解决的问题
组合、切割、排列、子集、棋盘(其中,组合不强调元素顺序,但排列强调元素顺序)
代码模板
(1)参数:一般先写逻辑,需要什么参数再写什么参数;返回值:一般为空
(2)终止条件:回溯法抽象为树形结构,一般到达叶子节点,找到满意的答案,就将答案保存
(3)回溯搜索的遍历过程:回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
def backtracking(参数) {
if 终止条件
存放结果
return
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小))
处理节点
backtracking(路径,选择列表) // 递归
回溯,撤销处理结果
}
经典错题
画属性图,横向是集合大小,纵向是递归深度!!
1. 组合类型题
(1)电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
解题思路:
回溯三部曲:
①参数及返回值:nums字符串、字符串s收集子路径,res收集所有路径,index遍历第几个数字;返回值一般为空
②终止条件:当index==len(nums),收集结果,返回递归
③回溯搜索的遍历过程:取出index指向的数字的字符集,遍历字符集,加入s,index指向下一个数字,进一步递归,递归结束后,撤销处理结果
代码:
class Solution:
def __init__(self):
self.dicts={
'2':'abc',
'3':'def',
'4':'ghi',
'5':'jkl',
'6':'mno',
'7':'pqrs',
'8':'tuv',
'9':'wxyz'
}
def letterCombinations(self, digits: str) -> List[str]:
if len(digits)==0: return []
return self.backtracking(digits,0,'',[])
def backtracking(self,digits,ind,s,res):
if ind==len(digits):
res.append(s)
return
num=digits[ind]
letters=self.dicts[num]
for i in range(len(letters)):
s+=letters[i]
self.backtracking(digits,ind+1,s,res)
s=s[:-1]
return res
(2)组合总和
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target
的不同组合数少于 150
个。
解题思路:
回溯三部曲:
①参数及返回值:集合candidites,目标值target,单一结果总和sum,单一结果path,结果集res,for循环起始位置startindex;返回值为空
**如果是一个集合求组和,需要用startindex,如果是多个集合求组合,各个集合之间互不影响,则不用startindex
②终止条件:sum>target和sum=target
③单层搜索逻辑:从startindex开始搜索candidates集合
④剪枝优化:若下一层的sum>target,则无需进入下一层递归
代码:
class Solution:
def backtracking(self, candidates, target, total, startIndex, path, result):
if total == target:
result.append(path[:])
return
for i in range(startIndex, len(candidates)):
if total + candidates[i] > target:
break
total += candidates[i]
path.append(candidates[i])
self.backtracking(candidates, target, total, i, path, result)
total -= candidates[i]
path.pop()
def combinationSum(self, candidates, target):
result = []
candidates.sort() # 需要排序
self.backtracking(candidates, target, 0, 0, [], result)
return result
(3)括号生成
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
解题思路:有效括号组合一定是左括号和右括号数目相同,并且以左括号开始的字符串
回溯三部曲:
①参数:当前括号数目i,左括号数目open,单一结果path,结果集res,括号对数n;返回值为空
②终止条件:当前括号数目等于2*n时,将path加入res,然后返回
③单一搜索逻辑:因为要以左括号开始,如果open<n,则将path[i]='(',递归(i+1,open+1,path,res,n)
如果i-open<open,即左右括号个数不匹配,则将path[i]=')',递归(i+1,open,path,res,n)
代码:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
path=['']*(2*n)
res=[]
return self.backtracking(n,0,0,path,res)
def backtracking(self,n,i,open,path,res): #美剧填左括号还是右括号
#参数:i表示当前填了几个括号,open表示当前填了几个左括号,i-open表示当前填了几个右括号
m=2*n
#终止条件:当i=m时,括号数目达到上限
if i==m:
res.append(''.join(path)) #将单一结果加入结果集
return
if open < n: #左括号未达到上限
path[i]='('
self.backtracking(n,i+1,open+1,path,res)
if i-open < open: #可以填右括号
path[i]=')'
self.backtracking(n,i+1,open,path,res)
return res
2. 排列类型题
(1)全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
解题思路:
回溯三部曲:
①参数及返回值:排列是有序的,每个元素都要使用,因此需要一个used数组记录该元素是否使用过,还需要nums数组;返回值一般为空
②终止条件:当path的长度等于nums的长度时,则找到一个全排列
③回溯搜索的遍历过程:遍历used,如果使用过->跳过;否则将当前节点标记为使用过,然后加入到path,递归搜索,递归返回后撤销操作
代码:
class Solution:
def permute(self, nums):
result = []
self.backtracking(nums, [], [False] * len(nums), result)
return result
def backtracking(self, nums, path, used, result):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
path.append(nums[i])
self.backtracking(nums, path, used, result)
path.pop()
used[i] = False
3. 子集问题
(1)子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
解题思路:子集也是一种组合问题,无序的,因此元素不能重复使用
回溯三部曲:
①参数及返回值:nums数组,path收集子路径,res收集所有路径,startind遍历开始索引;返回值一般为空
②终止条件:当startind大于nums数组长度,剩余集合为空,直接返回
③回溯搜索的遍历过程:处理节点-递归-回溯
(**收集子集要放在终止条件上面,否则会漏掉自己)
代码:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
return self.backtracking(nums,0,[],[])
def backtracking(self,nums,start,path,res):
res.append(path[:])
if start>len(nums)-1:
return
for i in range(start,len(nums)):
path.append(nums[i])
self.backtracking(nums,i+1,path,res)
path.pop()
return res
4. 切割问题
(1)分割回文串
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串。返回 s
所有可能的分割方案。
解题思路:(与组合问题类似)
回溯三部曲:
①参数:单一结果path,结果集res,切割线ind,字符串s
②终止条件:当切割线切到了字符串最后一个字符,找到一种切割方法,将结果加入结果集
③单层搜索逻辑:截取子串s[ind:i],判断是否为回文串,如果是加入path
判断回文子串的方法:双指针法,如果前后指针所指向的元素是相等的,就是回文字符串了。
代码:
class Solution:
def partition(self, s: str) -> List[List[str]]:
res=[]
self.backtracking(s,0,[],res)
return res
def backtracking(self,s,ind,path,res):
if ind==len(s):
res.append(path[:])
return
for i in range(ind,len(s)):
#判断是否为回文串
if self.substr(ind,i,s):
sub=s[ind:i+1]
path.append(sub)
self.backtracking(s,i+1,path,res)
path.pop()
def substr(self,start,end,strs): #双指针法
while start<end:
if strs[start]!=strs[end]:
return False
start+=1
end-=1
return True
5. 棋盘问题
(1)N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
解题思路:
回溯三部曲:
①递归参数:棋盘大小n,结果集res,遍历到第几行row
②终止条件:递归到棋盘最底层,收集结果返回
③单层搜索逻辑:row控制递归深度,for循环的遍历控制棋盘的列,每行从0开始搜索
验证棋盘是否合法:
不能同列
不能同斜线(45°或135°)
代码:
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
path=['.'*n for _ in range(n)]
res=[]
self.backtracking(n,path,res,0)
return [[''.join(row) for row in p] for p in res]
def backtracking(self,n,path,res,row):
#终止条件:到达棋盘底部
if row==n:
res.append(path[:])
return
for i in range(n):
if self.check_valid(row,i,path):
path[row]=path[row][:i]+'Q'+path[row][i+1:]
self.backtracking(n,path,res,row+1)
path[row]=path[row][:i]+'.'+path[row][i+1:]
def check_valid(self,row,col,path):
#不能同列
for i in range(row):
if path[i][col]=='Q':
return False
#不能同斜线 45°
i,j=row-1,col-1
while i>=0 and j>=0:
if path[i][j]=='Q':
return False
i-=1
j-=1
#135°
i,j=row-1,col+1
while i>=0 and j<len(path):
if path[i][j]=='Q' :
return False
i-=1
j+=1
return True