回溯
理论
1. 回溯三步曲
- 回溯函数模板返回值以及参数
- 回溯函数终止条件
- 回溯搜索的遍历过程
2. 代码模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
// for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历
3. 应用场景
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
组合问题
77. 组合
分析:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = []
path = []
def backtrack(start, end, knum):
if len(path) == k:
res.append(path[:]) # path[:]
return
for i in range(start, n+1):
path.append(i)
backtrack(i+1, n, knum-1) # 从i+1开始
path.pop()
backtrack(1,n,k) # 开始 终止 选k个
return res
代码优化(剪枝):
若开始start到结尾n序列长度(n-start+1)小于需要的长度(knum),则无需遍历了
即要求:n-start+1>=knum, 所以start<=n-knum+1 range左闭右开,所以剪枝操作为for i in range(start, n-knum+2)
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = []
path = []
def backtrack(start, end, knum):
if len(path) == k:
res.append(path[:]) # path[:]
return
for i in range(start, n-knum+2): # 优化(剪枝)
path.append(i)
backtrack(i+1, n, knum-1) # 从i+1开始
path.pop()
backtrack(1,n,k) # 开始 终止 选k个
return res
17. 电话号码的字母组合
class Solution:
def __init__(self):
self.answer = '' # 这里是字符串不是列表 最终结果answers才是列表形式
self.answers = []
self.map = {
'2':'abc',
'3':'def',
'4':'ghi',
'5':'jkl',
'6':'mno',
'7':'pqrs',
'8':'tuv',
'9':'wxyz'
}
def letterCombinations(self, digits: str) -> List[str]:
if not digits: # 空
return []
self.backtrack(digits, 0)
return self.answers
def backtrack(self, digits, index):
if index == len(digits): # 不是len(digits)-1 否则最后输出长度少1
self.answers.append(self.answer[:])
return
for letter in self.map[digits[index]]:
self.answer += letter # 字符串增加
self.backtrack(digits, index+1)
self.answer = self.answer[:-1] # 字符串去除最后一个元素 切片
注意字符串和列表的处理不同
39. 组合总和
终止条件:当和大于target直接跳出,若和等于target,该结果保留
class Solution:
def __init__(self):
self.answer = []
self.answers = []
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
n = len(candidates)
self.backtracking(0, n, candidates, target, 0)
return self.answers
def backtracking(self, startindex, n, candidates, target, add):
if add>target:
return
elif add == target:
self.answers.append(self.answer[:])
return
for i in range(startindex, n):
self.answer.append(candidates[i])
add += candidates[i]
self.backtracking(i, n, candidates, target, add) ## 下一次递归的起始为这一次放入answer中的索引 不是startindex
add -= candidates[i] ## 当前和也要回溯
self.answer.pop()
注意三个bug:
①self.answers.append(self.answer[:])
一定是有[:]
②self.backtracking(i, n, candidates, target, add)
递归时起点和当前处理的节点一致 i
③add -= candidates[i]
不仅answer回溯,和也要回溯
简化写法:
class Solution:
def __init__(self):
self.path = []
self.answers = []
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
self.backtracking(candidates, target)
return self.answers
def backtracking(self, candidates, target):
if target==0:
self.answers.append(self.path[:])
return
for index, num in enumerate(candidates):
if num<=target:
self.path.append(num)
self.backtracking(candidates[index:], target-num) # 为结果不重复,每次递归时候不遍历当前索引之前的元素
self.path = self.path[:-1]
else:
break # 如果不排序的话这里不能直接跳出,要继续遍历
40. 组合总和 II
注:和上一题区别,元素可以重复使用,一个元素可能出现多次,但结果要去重(涉及到同一层出现过的元素就不能再用)
class Solution:
def __init__(self):
self.answer = []
self.answers = []
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
n = len(candidates)
candidates.sort() # 对candidate进行排序 方便后面跳过重复的 实现去重
self.backtracking(0, n, candidates, target, 0)
return self.answers
def backtracking(self, startindex, n, candidates, target, sum_):
if target<sum_:
return
if target==sum_:
self.answers.append(self.answer[:])
return
for i in range(startindex, n):
if candidates[i] == candidates[i-1] and i>startindex: ## i>startindex 要对同一树层使用过的元素进行跳过
continue
self.answer.append(candidates[i])
sum_ += candidates[i]
self.backtracking(i+1, n, candidates, target, sum_)
self.answer.pop()
sum_ -= candidates[i]
注意三个bug:
①self.backtracking(i+1, n, candidates, target, sum_)
递归时起点在当前处理的节点后1
②if candidates[i] == candidates[i-1] and i>startindex:
同一数层,前面已经使用过的元素就不能再用了,否则会和前面的结果重复(如[10,1,2,7,6,1,5], target=8若不跳过第二个1,就会出现
[[1,1,6],[1,2,5],[1,7],[1,2,5],[1,7],[2,6]]),因为一个元素可用多次,为方便得出之前有无出现,要先对candidate排序
216. 组合总和 III
class Solution:
def __init__(self):
self.path = []
self.answer = []
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
self.backtracking(1, k, n, 0) # 最后的0是当前的和
return self.answer
def backtracking(self, start, k, n, sum_):
if sum_ > n or len(self.path)>k:
return
if sum_ == n and len(self.path)==k:
self.answer.append(self.path[:])
return
for i in range(start, 10-(k-len(self.path))+1): # 优化 若起始点到最后一个元素的长度要大于等于(k-len(self.path)) # for i in range(start, 10):
sum_ += i
self.path.append(i)
self.backtracking(i+1, k, n, sum_)
sum_ -= i
self.path.pop()
分割问题
131. 分割回文串
在上图做一点小改变,已分割过的之后不再出现,即,第二层为ab,b,[],这样不需要起始索引,只要递归的时候输入的s变化即可
class Solution:
def __init__(self):
self.path = []
self.answer = []
def partition(self, s: str) -> List[List[str]]:
self.backtracking(s)
return self.answer
def backtracking(self, s):
if not s: # 当传入的s为空时,说明遍历到了结尾,path结果添加到answer
self.answer.append(self.path[:])
return
for i in range(len(s)):
if self.isPalindrome(s[:i+1]): # 若是回文则添加
self.path.append(s[:i+1])
else:
continue
self.backtracking(s[i+1:]) # 每次待分割的s都会去掉之前已分割的部分
self.path.pop()
def isPalindrome(self, s): # 判断是否回文
i = 0
j = len(s)-1
while i<=j:
if s[i]==s[j]:
i += 1
j -= 1
else:
return False
return True
优化:使用动态规划保留以i起始,j结尾的字符串是否为回味子串,避免在判断回文时做一些重复操作
93. 复原 IP 地址
分析:
单层操作:对字符串s分割,前1个元素,前2个元素,……,如果遍历到前四个元素,则直接跳出这一层循环
若分割出来的字符串满足要求,则添加其至path中,并添加’.',然后进行递归操作,注意递归函数传的s是去掉已经使用过的部分,之后回溯(添加的点也要去除)
终止条件:如果遍历到第四段(所以要用一个splitnum记录已分割的段数),并且剩下的s也满足要求,则将已有的path加上s添加到answer中(注:由于没有真正地将s添加至path,所以这一步不需要回溯),return
class Solution:
def __init__(self):
self.path= ''
self.answer = []
def restoreIpAddresses(self, s: str) -> List[str]:
self.backtracking(s, 0)
return self.answer
def backtracking(self, s, splitnum):
if len(s)>(4-splitnum)*3: # 剪枝(可加可不加)
return
if splitnum == 3:
if self.isValid(s):
self.answer.append(self.path[:] + s)
return
else:
return
for i in range(len(s)):
if i>2: #此时有四个元素 直接跳出这一层 剪枝
break
if self.isValid(s[:i+1]):
self.path += s[:i+1]
self.path += '.'
self.backtracking(s[i+1:], splitnum + 1)
self.path = self.path[:-(i+1)-1]
def isValid(self, s):
if len(s)>1 and s[0] == '0':
return False
if s and 0<=int(s[:])<=255:
return True
return False
小结:这两个分割问题中注意递归函数的待分割对象改变(无需使用startindex标记)
子集问题
78. 子集
class Solution:
def __init__(self):
self.path = []
self.answer = [[]] #空集是任何一个集合的子集
def subsets(self, nums: List[int]) -> List[List[int]]:
self.backtracking(nums)
return self.answer
def backtracking(self, nums):
if not nums: # nums为空,则跳出
return
for i in range(len(nums)):
self.path.append(nums[i])
self.answer.append(self.path[:]) # 每个路径都添加到answer中,而不是最后的path
self.backtracking(nums[i+1:])
self.path.pop()
90. 子集 II
分析:和78.子集区别就是集合里有重复元素了,而且求取的子集要去重。画图可以看出,同一树层上重复的数字要过滤掉,同一树枝上就可以重复取,因为同一树枝上元素的集合才是唯一子集!
思考:这里的去重操作和40.数组总和Ⅱ同,先排序,后续遍历到和之前一样的直接跳过不做处理
class Solution:
def __init__(self):
self.path = []
self.answer = [[]]
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort() # 排序 便于之后去重
self.backtracking(nums)
return self.answer
def backtracking(self, nums):
if not nums:
return
for i in range(len(nums)):
if i>0 and nums[i]==nums[i-1]: # 若同一层一个分支和前一个相同,则直接跳过,进入下一个分支 去重
continue
self.path.append(nums[i])
self.answer.append(self.path[:])
self.backtracking(nums[i+1:])
self.path.pop()
491. 递增子序列
分析:数组nums有重复元素,但输出要求去重,不能直接利用之前的思路对其排序,同层遍历相邻相同的直接跳过,因为输出要递增,所以可以使用集合/哈希表记录同层已出现的元素
元素添加到path的条件是要小于等于path末尾的元素
path添加到answer的条件是长度大于等于2
class Solution:
def __init__(self):
self.path = []
self.answer = []
def findSubsequences(self, nums: List[int]) -> List[List[int]]:
self.backtracking(nums)
return self.answer
def backtracking(self, nums):
if not nums:
return
# 层遍历
set_ = set() # 每层都有一个set_用于记录已经出现过的元素
for i in range(len(nums)):
if nums[i] in set_: # 对一个数层中重复出现的元素直接跳过
continue
if self.path and nums[i]<self.path[-1]: # path中有值,且当前元素小于path中的值 则不满足递增要求
continue
self.path.append(nums[i])
set_.add(nums[i]) # 使用过的元素放在set_中
if len(self.path)>=2:
self.answer.append(self.path[:])
self.backtracking(nums[i+1:])
self.path.pop()
注意用于记录每层出现过的元素的集合set_的设置,在每层for循环前
子集问题小结:
注意画图,每次只选一个元素
去重操作:同层元素不能相同(对一个节点而言)
①输出有序,则不能先对nums排序,只能用集合记录同层已出现的元素实现去重②输出无序,可以先对nums排序,同层中若nums[i]==nums[i-1]则continue
排列问题
46. 全排列
注意:这里是纵向去重,同一个树枝不能出现已出现过的元素if nums[i] not in self.path
class Solution:
def __init__(self):
self.path = []
self.answer = []
def permute(self, nums: List[int]) -> List[List[int]]:
self.backtracking(nums)
return self.answer
def backtracking(self, nums):
if len(self.path)==len(nums):
self.answer.append(self.path[:])
return
for i in range(len(nums)):
if nums[i] not in self.path: # 纵向去重
self.path.append(nums[i])
self.backtracking(nums)
self.path.pop()
另一种写法:
class Solution:
def __init__(self):
self.path = []
self.answer = []
def permute(self, nums: List[int]) -> List[List[int]]:
self.backtracking(nums)
return self.answer
def backtracking(self, nums):
if not nums:
self.answer.append(self.path[:])
return
for i in range(len(nums)):
self.path.append(nums[i])
midnums = nums[:i]
midnums.extend(nums[i+1:]) # 去掉nums[i]的新数组
self.backtracking(midnums)
self.path.pop()
python语法:两个数组合并 a.extend(b),注意a改变
a = [1,2,3,4,7,5,6]
b = ['a','b']
c = ['h',12,'c']
a.extend(b)
a.extend(c)
print(a)
#结果:[1, 2, 3, 4, 7, 5, 6, 'a', 'b', 'h', 12, 'c']
47. 全排列 II
分析:46. 全排列基础上同数层去重(可用哈希表也可对原nums排序(因为结果可按任意顺序))
class Solution:
def __init__(self):
self.path = []
self.answer = []
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
self.backtracking(nums)
return self.answer
def backtracking(self, nums):
if not nums:
self.answer.append(self.path[:])
return
set_ = set()
for i in range(len(nums)):
if nums[i] not in set_:
set_.add(nums[i])
self.path.append(nums[i])
midnums = nums[:i]
midnums.extend(nums[i+1:])
self.backtracking(midnums)
self.path.pop()
else:
continue
棋盘问题(有点复杂,hot100无)
51. N皇后
class Solution:
def __init__(self):
self.path = [] # 存放每行皇后所在的位置
self.answer = []
def solveNQueens(self, n: int) -> List[List[str]]:
if n==1:
return [["Q"]]
elif n==2 or n==3:
return []
self.backtracking(n)
return self.answer
def backtracking(self, n):
if n==len(self.path):
self.answer.append([f"{'.'*(col)}Q{'.'*(n-col-1)}" for col in self.path])
return
for i in range(n):
# 去掉不满足要求的
if i in self.path: # 去掉同列的
continue
### 这个不在同一斜线上的判断很重要!!!易写错
pos = i # 当前所处的列
sign = True
# 判断当前列的左45°是否有皇后
for row in range(len(self.path)-1, -1, -1): # pos-1 是上一行的皇后不能在的列
if pos-1==self.path[row]:
sign = False
break
else:
pos -= 1
if sign == False:
continue
pos = i # 当前所处的列 # 注意上面已改变pos
# 判断当前列的右45°是否有皇后
for row in range(len(self.path)-1, -1, -1):
if pos+1==self.path[row]:
sign = False
break
else:
pos += 1
if sign == False:
continue
self.path.append(i)
self.backtracking(n)
self.path.pop()
代码bug:对是否在同一斜线的判断,输出answer的表示
52. N皇后 II
与51不同的是不要求返回排列,只要求返回满足要求的解决方案的数量,与上题基本误差,answer是数目(甚至这道题更简单)
37. 解数独(待做,hot100无)
其他
79. 单词搜索
class Solution:
def __init__(self):
self.res = False
def exist(self, board: List[List[str]], word: str) -> bool:
m = len(board)
n = len(board[0])
matrix = [[0]*n for _ in range(m)]
for i in range(m):
for j in range(n):
self.backtracking(board, word, 0, i, j, matrix) # 以board[i][j]为起点
return self.res
def backtracking(self, board, word, index, i, j, matrix):
if word[index] != board[i][j]: # board[i][j]和word[index]不匹配
return
if index == len(word)-1: # 最后一个word也已匹配
self.res = True
return
matrix[i][j] = 1 # 使用过的位置的matrix设为1
if i-1>=0 and matrix[i-1][j] != 1: # 上面节点未超过边界且未被加入path
self.backtracking(board, word, index+1, i-1, j, matrix)
if self.res == True: # 可不加
return
if j+1<len(board[0]) and matrix[i][j+1] != 1: # 右面节点未超过边界且未被加入path
self.backtracking(board, word, index+1, i,j+1, matrix)
if self.res == True:
return
if i+1<len(board) and matrix[i+1][j] != 1: # 下面节点未超过边界且未被加入path
self.backtracking(board, word, index+1, i+1, j, matrix)
if self.res == True:
return
if j-1>=0 and matrix[i][j-1] != 1: # 左面节点未超过边界且未被加入path
self.backtracking(board, word, index+1, i,j-1, matrix)
if self.res == True:
return
matrix[i][j] = 0 # 回溯