什么是回溯算法?
回溯算法是一种经典的解决组合优化问题、搜索问题以及求解决策问题的算法。它通过不断地尝试各种可能的候选解,并在尝试过程中搜索问题的解空间,直到找到问题的解或者确定问题无解为止。回溯算法常用于解决诸如排列、组合、子集、棋盘类等问题。
下面是用Python实现回溯算法的示例代码,并对其进行详细解释:
def backtrack(candidate, path, result):
if 满足结束条件: # 如果已经满足结束条件
result.append(path[:]) # 将当前路径添加到结果中
return
for 选择 in 候选集: # 遍历所有候选选择
if 当前选择合法: # 如果当前选择是合法的
做出选择 # 将当前选择添加到路径中
backtrack(新的候选集, 新的路径, 结果) # 递归调用,继续探索下一层决策树
撤销选择 # 回溯,撤销当前选择,尝试其他选择
上述代码是回溯算法的一般模板。详细解释每个部分的含义:
1.backtrack 函数:这是回溯算法的核心函数。它接受三个参数:candidate 是当前的候选集,path 是当前的路径,result 是保存最终结果的列表。
2.结束条件:在 backtrack 函数的开头,我们先判断是否满足结束条件。如果满足结束条件,即已经找到了一个解,就将当前路径 path 添加到结果 result 中,并立即返回。
3.遍历候选集:在回溯算法中,我们会对当前的候选集进行遍历,尝试每一种可能的选择。
4.合法性检查:在遍历候选集的过程中,我们会对每一个候选选择进行合法性检查,判断当前选择是否符合问题的要求。
5.做出选择:如果当前选择是合法的,我们会将其添加到路径中,表示我们已经做出了一次选择。
6.递归调用:接着,我们会递归调用 backtrack 函数,继续探索下一层决策树。在递归调用中,我们会更新候选集、路径等参数,以便于进行下一层的选择。
7.撤销选择:在递归调用返回后,表示我们已经探索完了当前选择所导致的所有可能性,需要进行回溯。在回溯的过程中,我们会撤销当前选择,尝试其他选择,以便进一步探索其他可能的解。
这就是回溯算法的基本实现思路。通过不断地尝试各种可能的选择,并在尝试过程中搜索解空间,回溯算法能够有效地解决各种组合优化问题和搜索问题。
经典例题
1.N 皇后问题
给定一个 N × N 的棋盘,在棋盘上放置 N 个皇后,使得它们互相不能攻击,即任意两个皇后不能处于同一行、同一列或同一斜线上。
递归回溯:在每一行放置一个皇后,递归地处理下一行的放置。
合法性检查:在放置皇后时,检查当前位置是否与已放置皇后冲突。
回溯撤销:如果当前位置不能放置皇后,则撤销当前选择,尝试其他选择。
def solve_n_queens(n):
def is_valid(row, col, queens):
for r, c in queens:
if r == row or c == col or abs(row - r) == abs(col - c):
return False
return True
def backtrack(row, queens):
if row == n:
result.append(queens[:])
return
for col in range(n):
if is_valid(row, col, queens):
queens.append((row, col))
backtrack(row + 1, queens)
queens.pop()
result = []
backtrack(0, [])
return [['.' * col + 'Q' + '.' * (n - col - 1) for row, col in solution] for solution in result]
# 测试
n = 4
print(solve_n_queens(n))
1.solve_n_queens 函数接受一个参数 n,表示棋盘的大小。
2.is_valid 函数用于检查当前位置 (row, col) 是否与已放置的皇后冲突,如果冲突则返回 False,否则返回 True。
3.backtrack 函数是回溯的核心函数,它递归地在每一行放置皇后,并进行合法性检查,如果合法则继续放置下一行的皇后,如果不合法则进行回溯撤销。
最后,返回所有合法的解。每个解使用二维列表表示,其中每个列表元素表示棋盘中一行的布局,‘Q’ 表示放置了皇后的位置,‘.’ 表示空白位置。
queens.pop() 操作用于回溯过程中的撤销选择,以准备进行下一次选择的尝试。
具体来说,在解决 N 皇后问题的回溯算法中,我们使用 queens 列表来存储每一行皇后的位置。在 backtrack()函数中,我们逐行尝试放置皇后,并在每一行中遍历所有列来找到合适的位置。
当找到一个合法的皇后位置时,我们将其添加到 queens 列表中,表示该行放置了一个皇后。然后,我们通过递归调用 backtrack()
函数来处理下一行。但在处理下一行之前,我们需要撤销当前行的选择,以便进行下一次选择的尝试。这就是通过执行 queens.pop() 操作来实现的。queens.pop() 会将列表中的最后一个元素(即当前行的皇后位置)移除,以回到上一行尝试其他列的选择。
这个撤销选择的过程非常重要,因为我们需要尝试所有可能的组合。通过不断地添加和移除元素,我们可以探索不同的皇后放置方式,直到找到所有合法的解。
总结起来,queens.pop() 操作用于回溯过程中撤销当前行的选择,以准备进行下一次选择的尝试。它确保在下一次递归调用之前,queens
列表不包含上一次尝试的选择。
2.组合总和
给定一个候选数组 candidates 和一个目标数 target,找出候选数组中所有可以使数字和为目标数的组合。同一个数字可以被选取多次。
递归回溯:在每一层递归中,尝试使用当前候选数组中的数字来组合成目标数。
剪枝优化:在递归过程中,如果当前数字大于目标数,则可以提前结束递归。
去重处理:为了避免重复的组合,我们可以规定每次选择的数字必须不小于上一个选择的数字。
def combination_sum(candidates, target):
def backtrack(start, path, target):
if target == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > target:
continue
path.append(candidates[i])
backtrack(i, path, target - candidates[i])
path.pop()
result = []
candidates.sort()
backtrack(0, [], target)
return result
# 测试
candidates = [2, 3, 6, 7]
target = 7
print(combination_sum(candidates, target))
1.combination_sum 函数接受两个参数:candidates 是候选数组,target 是目标数。
2.backtrack 函数是回溯的核心函数,它接受三个参数:start 表示当前可选的起始索引,path 是当前的组合,target 是当前的目标数。
3.在 backtrack 函数中,如果 target 等于 0,则表示已经找到了一个组合,将当前组合 path 添加到结果列表 result 中。
4.然后,我们遍历候选数组中的数字,并递归调用 backtrack 函数进行下一层的组合。在递归调用中,我们更新 start 参数,以避免重复的组合。
5.如果当前数字大于目标数,则直接跳过,进行剪枝优化。
最后,返回所有找到的组合。
当进行递归回溯时,我们将当前的选择添加到路径 (path) 中,然后递归地继续向下搜索。但是,如果发现当前的选择不符合要求或者已经达到目标,我们需要撤销这个选择并回退到之前的状态,以便尝试其他可能的选择。这就是通过使用 pop() 操作从路径中移除最后一个元素来实现的。
具体来说,在给定的代码中,在每次递归的开始,我们将 candidates[i] 添加到 path 中。然后,我们通过递归调用backtrack() 函数继续向下搜索。如果在搜索的过程中发现了一个满足条件的结果,我们将其添加到 result 列表中。无论是否找到结果,最后一步都是执行 path.pop(),将最后一个元素从 path 中移除,以进行回溯。
这样做的目的是确保在回溯之后,path 不包含已经尝试过的选择,以便进行下一次选择的尝试。通过不断地添加和移除元素,我们可以探索不同的组合路径,直到找到所有满足条件的结果。
3.全排列
给定一个没有重复数字的序列,返回其所有可能的全排列
递归回溯:在每一层递归中,尝试使用当前可选的数字进行排列。
标记已使用:在递归过程中,需要标记已经使用过的数字,避免重复使用。
处理结果:当达到排列长度时,将当前排列添加到结果列表中。
def permute(nums):
def backtrack(path):
if len(path) == len(nums):
result.append(path[:])
return
for num in nums:
if num in path:
continue
path.append(num)
backtrack(path)
path.pop()
result = []
backtrack([])
return result
# 测试
nums = [1, 2, 3]
print(permute(nums))
1.permute 函数接受一个参数 nums,表示输入的序列。
2.backtrack 函数是回溯的核心函数,它接受一个参数 path,表示当前的排列。
3.在 backtrack 函数中,如果当前排列长度等于输入序列长度,则表示已经找到一个全排列,将其添加到结果列表 result 中。
4.然后,我们遍历输入序列中的数字,并递归调用 backtrack 函数进行下一层的排列。在递归调用中,我们使用 path 参数来标记已经使用过的数字,避免重复使用。
5.最后,返回所有找到的全排列。
4.子集
给定一个数组,返回其所有可能的子集
递归回溯:在每一层递归中,尝试加入当前元素或不加入当前元素。
处理结果:当递归到底层时,将当前子集添加到结果列表中。
def subsets(nums):
def backtrack(start, path):
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
result = []
backtrack(0, [])
return result
# 测试
nums = [1, 2, 3]
print(subsets(nums))
5.电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合
递归回溯:在每一层递归中,尝试加入当前数字对应的字母。
处理结果:当递归到底层时,将当前字母组合添加到结果列表中。
def letter_combinations(digits):
if not digits:
return []
phone_map = {'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'}
def backtrack(index, path):
if index == len(digits):
result.append(''.join(path))
return
for char in phone_map[digits[index]]:
path.append(char)
backtrack(index + 1, path)
path.pop()
result = []
backtrack(0, [])
return result
# 测试
digits = "23"
print(letter_combinations(digits))
6.岛屿数量
给定一个由 ‘0’ 和 ‘1’ 组成的二维网格地图,其中 ‘1’ 表示陆地,‘0’ 表示水域,计算岛屿的数量。岛屿被水域包围,并且水平或垂直相邻(不包含对角线)的陆地被认为是同一个岛屿。
实现思路:
DFS:遍历整个网格,当遇到陆地时,进行深度优先搜索,将与当前陆地相连的所有陆地标记为已访问,直到所有相连的陆地被访问完毕为止。
计数:每次找到一个新的岛屿时,增加岛屿数量。
def num_islands(grid):
def dfs(row, col):
if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]) or grid[row][col] == '0':
return
grid[row][col] = '0' # 标记为已访问
for dr, dc in directions:
dfs(row + dr, col + dc)
if not grid:
return 0
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
num_rows, num_cols = len(grid), len(grid[0])
num_islands = 0
for row in range(num_rows):
for col in range(num_cols):
if grid[row][col] == '1':
num_islands += 1
dfs(row, col)
return num_islands
# 测试
grid = [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
]
print(num_islands(grid))
7.单词搜索
给定一个二维网格和一个单词,判断单词是否存在于网格中。字母相邻(包括对角线)的格子组成了单词
实现思路:
对于网格中的每个格子,都作为起点尝试进行深度优先搜索。
在深度优先搜索的过程中,递归地探索当前格子的上、下、左、右四个相邻格子,判断是否能够匹配单词中的下一个字母。
如果能够匹配,继续向下递归搜索;如果不能匹配或者超出了边界,则回溯到上一个格子,尝试其他方向的搜索。
在搜索的过程中,需要使用一个额外的标记数组来记录已经访问过的格子,避免重复访问。
def exist(board, word):
def dfs(row, col, index):
# 终止条件:单词已全部匹配
if index == len(word):
return True
# 边界条件:越界或当前字母不匹配
if row < 0 or row >= len(board) or col < 0 or col >= len(board[0]) or board[row][col] != word[index]:
return False
# 临时标记当前位置已访问
temp = board[row][col]
board[row][col] = '#'
# 递归搜索当前字母的上、下、左、右四个相邻格子
for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
if dfs(row + dr, col + dc, index + 1):
return True
# 回溯:恢复原始状态
board[row][col] = temp
return False
if not board or not word:
return False
# 逐个尝试每个格子作为起点
for row in range(len(board)):
for col in range(len(board[0])):
if dfs(row, col, 0):
return True
return False
# 测试
board = [
['A', 'B', 'C', 'E'],
['S', 'F', 'C', 'S'],
['A', 'D', 'E', 'E']
]
word = "ABCCED"
print(exist(board, word)) # 输出: True