回溯法是深度优先遍历中的一种特有现象,主要用于在一个较大的数据集中寻找满足特定条件的解。回溯法就是当前状态不满足条件时,就回到上一个状态,即回到过去,然后再次向下搜索。在此过程中,需要用到决策树(因为在每一个节点上,都要做出决策要走哪一条路径),在该树上完成向上回溯,其实就是多叉树的遍历问题。
回溯算法在遍历的过程中,如果能够提前判断这一条路径不满足结果,就可以提前结束向下的遍历,即剪枝,可以适当较少复杂度。回溯法与枚举法的区别就在于,枚举是一直走到最后才回退寻找其余路径,直到尝试完所有路径。而回溯可以通过剪枝提前确定这条路是否满足条件,如果不满足,就不再在该条路上继续遍历。
回溯算法的模板:
res= [] # 结果路径集
def backtrack(路径, 待选列表):
if 满足结束条件:
res.append(路径)
return
for 选择 in 待选列表:
选择一个待选路径放入结果集中,并将该路径从待选列表中移除
将该选择加入到结果路径集中
backtrack(路径, 待选列表)
将刚刚选择的路径再加入到待选列表中
(撤销选择,因为在回到上一层节点的时候,需要重置上一次的选择,即不满足就回退到上一步)
例题:
全排列:https://leetcode-cn.com/problems/permutations/
问题:给定一个 没有重复 数字的序列,返回其所有可能的全排列。
1、画出决策树,遇到每一个节点时判断是否满足结束条件(当前选择中包含所有待选列表的值),如果满足,就加入结果列表集。如果不满足,继续向下一层遍历并选择。
2、(根据模板)遍历待选列表中的值,选择一个放入当前选择列表中,然后将该值从待选列表中删除,加入到当前选择列表中。然后递归实现。之后撤销之前的操作,即回到上一层节点继续向下遍历。
public class Solution{
List<List<Integer>> permute(int[] nums) {
LinkedList<Integer> track = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();
backtrack(nums, track, res);
return res;
}
void backtrack(int[] nums, LinkedList<Integer> track, List<List<Integer>> res) {
if (track.size() == nums.length) {
res.add(new LinkedList<>(track));
return;
}
for (int i = 0; i < nums.length; i++) {
if (track.contains(nums[i]))
continue;
track.add(nums[i]);
backtrack(nums, track, res);
track.removeLast();
}
}
}
全排列 II:https://leetcode-cn.com/problems/permutations-ii/
问题:给定一个可包含重复数字的序列,返回所有不重复的全排列
剪枝的条件:当前元素和钱一个元素值相同且前一个没有被使用:nums[i] == nums[i-1] and check[i-1] == 0
递归结束条件:当前结果集长度等于候选列表长度:len(path) == len(nums)
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
check = [0]*len(nums)
self.backtrack([], nums, check, res)
return res
def backtrack(self, path, nums, check, res):
if len(path) == len(nums):
res.append(path[:])
return
for i in range(len(nums)):
if check[i] == 1:
continue
if i > 0 and nums[i] == nums[i-1] and check[i-1] == 0:
continue
check[i] = 1
path.append(nums[i])
self.backtrack(path, nums, check, res)
path.pop()
check[i] = 0
8皇后问题。
在8*8的棋盘上放8个皇后,且其中任一皇后不能吃掉其余皇后。 皇后可以吃掉同一行、同一列和对角线上的其他皇后。 问:共有多少种放法。
分析:
1.从第0行开始遍历,如果可以循环到最后一行,则说明该条路径满足题意,就输出该结果 。
2.定义一个临时棋盘,用来遍历该行中每一列的所有子情况,
每次循环子情况的时候需要初始化棋盘,将当前行放置的皇后清空,然后将棋盘 [当前行][列]置为1,然后检查循环的当前列是否能放置皇后 。如果不能,就进行下一列循环;如果不能,就回到上一行,即回溯,再重新选择下一列进行遍历。
代码如下:
# 判断当前位置能否放皇后
def isSafety(chess, row, col):
step = 1
while row - step >= 0:
if chess[row - step][col] == 1:
return False
if col - step >= 0 and chess[row - step][col - step] == 1:
return False
if col + step < 8 and chess[row - step][col + step] == 1:
return False
step += 1
return True
def putQueen(chess, row):
# 如果循环到最后一行,就输出结果
if row == 7:
print("===================================")
for i in range(8):
print(chess[i])
return
# 如果不是最后一行,则新建临时数组,遍历子情况
temp = chess.copy()
# temp = [[0] * 8 for i in range(8)]
# 遍历当前行所有子情况
for i in range(8):
for j in range(8):
temp[row][j] = 0
chess[row][i] = 1
# 如果可以,就放皇后
if isSafety(chess, row, i):
putQueen(chess, row + 1)
if __name__ == '__main__':
count = 0
chess = [[0] * 8 for i in range(8)]
putQueen(chess, 0)
组合总和:https://leetcode-cn.com/problems/combination-sum/comments/
给定一个无重复元素的数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。candidates
中的数字可以无限制重复被选取
代码解析如下:
# for循环和递归配合可以实现回朔:
# 当递归从递归出口出来之后,上一层的for循环就会继续执行。
# 而for循环的继续执行就会给出当前节点下的下一条可行路径。而后递归调用,就顺着这条从未走过的路径又向下走一步。
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
n = len(candidates)
if n == 0:
return []
candidates.sort()
path,res = [],[]
self.track(0,candidates,path,res,target)
return res
def track(self,start,candidates,path,res,target):
if 0 == target: #递归终止条件
res.append(path[:])
return
for i in range(start,len(candidates)): # 走到start节点
if target - candidates[i] < 0: # 如果当前节点不满足条件,就不再继续向下搜索
return
path.append(candidates[i]) # 如果满足条件,将当前位置放入结果列表
self.track(i,candidates,path,res,target-candidates[i])#从该节点继续向下走
path.pop() # 回溯,回到上一层,所以将当前节点删除
组合总和 II:https://leetcode-cn.com/problems/combination-sum-ii/
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
分许:
与上一题的不同之处在于,本题中每个数字在每个组合中只能使用一次,即要去重。其余的思路不变,只是在for循环中,要判断同一层中是否出现相同的值,确保同一层(i > start)中没有相同的元素(candidates[i] == candidates[i-1])。
本题可以采用加的形式,按照顺序依次加上数组中的数字,直到大于等于target,如果等于target,就把等于target的列表结果加入到结果集中。
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
if not candidates:
return []
candidates.sort()
n = len(candidates)
res = []
def backtrack(start, tsum, tmp_list):
if tsum == target: # 相加等于目标值,递归结束
res.append(tmp_list)
return
for i in range(start, n):
if tsum + candidates[i] > target: # 不满足,剪枝,改层递归结束
break
# 在一个for循环中,所有被遍历到的数都在同一层。一个层级中,不能出现相同的元素。
if j > start and candidates[i] == candidates[i-1]:
continue
backtrack(i + 1, tsum + candidates[i], tmp_list + [candidates[i]])
backtrack(0, 0, [])
return res
子集:https://leetcode-cn.com/problems/subsets/
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
分析:本题就是求决策树中所有的节点值
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res,path = [],[]
self.track(0,[],res,nums)
return res
def track(self,start, path,res,nums):
res.append(path[:])
for i in range(start,len(nums)): # i从start开始遍历
path.append(nums[i]) # 加入选择列表
self.track(i+1,path,res,nums) # 回溯
path.pop() # 回到上一个状态
子集 II:https://leetcode-cn.com/problems/subsets-ii/
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
分析:
去重:在for循环中,要判断同一层中是否出现相同的值,确保同一层(i > start)中没有相同的元素(nums[i] == nums[i-1])。
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
if not nums:
return []
res = []
nums.sort()
def traceback(start, path):
if len(path) <= len(nums): # 如果满足条件,就将结果加入到结果集中
res.append(path[:])
for i in range(start, len(nums)):
# 在当前层级下(相同的递归深度),如果前后字符是相同的,
# 那么跳过这个字符(因为已经处理过了),不能重复添加到path中。
if i > start and nums[i] == nums[i-1]:
continue
path.append(nums[i])
traceback(i + 1, path)
path.pop()
traceback(0, [])
return res