回溯法
回溯法的基本思想:
- 从一条路往前走,能进则走,不能进则退回来,换一条路再试。
- 八皇后问题就是回溯法的典型,第一步按照顺序放一个皇后,然后第二步符合要求放第二个皇后,如果没有位置符合要求,改变第一个皇后的位置,重新放第2个皇后的位置,直到找到符合要求的位置就可以了。
- 回溯法在迷宫问题中使用很常见,就是一条路走不通,然后返回到前一个路口,继续下一条路。
- 回溯法说白了就是穷举法。不过回溯法使用剪枝函数,减去一些不可能达到最终状态的节点,从而减少状态空间树节点的生成。
- 回溯法是一个既带有系统性又带有跳跃性的搜索函数。它在包含问题的所有解的解空间中,按照深度优先的策略,从根节点出发搜索解空间树。算法搜索至解空间的任意节点时,总是先判断该节点是否肯定不包含问题的解。如果肯定不包含,则跳过该节点为根的子树的系统搜索,逐层向其祖先节点回溯。否则,进入该子树,继续按照深度优先的策略进行搜索。回溯法用来求问题的所有解时,要回溯到根,且根节点的所有子树都已经被搜索遍才结束。而回溯法在用来求问题的任意一解时,只要搜索到问题的一个解就可以结束了。这种以深度优先的方式系统地搜索问题解空间的解法称为回溯法,适合于组合数较大的问题。
回溯法的算法题:
-
46. 全排列
思路:从数组的第一个数字开始依次跟后面的所有数字交换位置,然后从第二个数字开始再依次跟后面所有的数字交换位置,直到所有的数字都交换位置后,每次交换位置后都要回溯到数组的原始位置。
class Solution: def permute(self, nums: List[int]) -> List[List[int]]: if not nums: return [] res = [] l = 0 n = len(nums) def trackback(l): #回溯结束的条件: if l==n-1: res.append(nums[:]) return for i in range(l, n): nums[i], nums[l] = nums[l], nums[i] trackback(l+1) nums[i], nums[l] = nums[l], nums[i] trackback(l) return res
-
39. 组合总和
思路:首先为避免组合重复,将数组进行排序。将累加和变量初始化为0,从数组的第一个元素开始,跟累加和变量curr_sum求和,先判断将当前元素加入到curr_sum中,会不会大于target,如果大于,则递归结束,并将path中已有的元素删除,也就是回溯到path原有的状态,并开始下一个迭代。如果小于,则将当前元素加入到path中,继续向下回溯。如果等于则将path的结果放入最终的结果res中。
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
def trackback(l, size, curr_sum):
if curr_sum==target:
res.append(path[:])
return
for i in range(l, size):
if curr_sum + candidates[i] > target: # 剪枝
break
path.append(candidates[i])
trackback(i, size, curr_sum + candidates[i]) #深度优先搜索
path.pop()
res = []
if not candidates:
return res
candidates.sort()
path = []
size = len(candidates)
curr_sum = 0
trackback(0, size, curr_sum)
# print(res)
return res
-
78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: 输入: nums = [1,2,3] 输出: [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]
class Solution: def subsets(self, nums: List[int]) -> List[List[int]]: res = [[]] path = [] l = 0 n = len(nums) nums.sort() def trackback(path, l): for i in range(l, n): path.append(nums[i]) res.append(path[:]) trackback(path, i+1) #深度优先搜索 path.pop() trackback(path, l) return res
-
90. 子集 II
class Solution: def subsetsWithDup(self, nums: List[int]) -> List[List[int]]: if not nums: return [] res = [[]] path = [] l = 0 n = len(nums) nums.sort() def trackback(path, l): for i in range(l, n): if i>=l+1 and nums[i]==nums[i-1]: # 先判断当前解是否满足条件,满足条件才将当前解加入解空间,否则跳过 continue path.append(nums[i]) res.append(path[:]) trackback(path, i+1) path.pop() trackback(path, l) return res
思路:
首先将网格的所有节点都初始化为未标记,然后从网格的第一个节点开始搜索,如果网格的某个节点跟目标字符串的第一个字符相同,则将该节点进行标记,然后用深度优先搜索策略分别从该节点的上下左右四个方向进搜索下一个字符,如果其中任意一个方向的节点跟字符串的下一个字符相同,则对该方向上的节点进行标记,然后继续按照上述步骤往下搜索,直到字符串中的所有字符都匹配为止,则返回True.其中只要有一个字符没有搜索到,则不往下继续搜索,并依次将之前已经标记过的节点回溯到未标记状态,重新从网格的下一个节点开始搜索。如果整个网格搜索完毕,目标字符串该有未匹配的字符,则算法输出False
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
self.directs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
m = len(board)
if m == 0:
return False
n = len(board[0])
mark = [[0 for _ in range(n)] for _ in range(m)]
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == word[0]:
# 将该元素标记为已使用
mark[i][j] = 1
if self.backtrack(i, j, mark, board, word[1:]) == True:
return True
else:
# 回溯
mark[i][j] = 0
return False
def backtrack(self, i, j, mark, board, word):
if len(word) == 0:
return True
for direct in self.directs:
cur_i = i + direct[0]
cur_j = j + direct[1]
if cur_i >= 0 and cur_i < len(board) and cur_j >= 0 and cur_j < len(board[0]) and board[cur_i][cur_j] == word[0]:
# 如果是已经使用过的元素,忽略(判断当前解是否满足条件)
if mark[cur_i][cur_j] == 1:
continue
# 将该元素标记为已使用
mark[cur_i][cur_j] = 1
if self.backtrack(cur_i, cur_j, mark, board, word[1:]) == True: #深度优先搜索
return True
else:
# 回溯
mark[cur_i][cur_j] = 0
return False
216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
res = []
length = n if n<10 else 10
l = 1
curr_sum = 0
path = []
def trackback(path, l, length, curr_sum):
if len(path) == k: # 解空间
res.append(path[:])
return
for num in range(l, length):
if curr_sum+num>n: # 剪枝
break
elif curr_sum+num<n and len(path)==k-1: # 判断当前解是否满足条件
continue
path.append(num)
trackback(path, num+1,length, curr_sum+num)
path.pop()
trackback(path, l, length, curr_sum)
return res