回溯算法是一种通过试错来解决问题的算法。它会在解决问题的过程中剪枝,以避免无效搜索。在Python中实现回溯算法通常涉及以下几个步骤:
定义问题:明确你要解决的问题,比如八皇后问题、数独求解、组合问题等。
建立递归结构:回溯算法通常以递归函数的形式实现。这个递归函数会尝试不同的解决方案,并在解决方案不可行时回溯。
剪枝:在递归过程中,如果发现当前路径不可能产生正确的完整解决方案,就放弃这条路径,回溯到上一步。
记录解:一旦找到一个完整的解决方案,就将其记录下来。
回溯:从最后一步向前撤销,尝试其他可能的选项。
下面是一个简单的回溯算法的例子,用于解决一个组合问题:给定一组不重复的整数,找出所有可能的组合,其和为特定目标数。
def combination_sum(candidates, target):
def backtrack(remain, combo, start):
if remain == 0: # 找到一个解
result.append(list(combo))
return
elif remain < 0: # 超过目标值,剪枝
return
for i in range(start, len(candidates)):
combo.append(candidates[i]) # 选择当前数字
backtrack(remain - candidates[i], combo, i) # 继续寻找下一个数字
combo.pop() # 撤销选择,回溯
candidates.sort() # 对数字进行排序,有助于剪枝
result = []
backtrack(target, [], 0)
return result
# 示例
candidates = [2, 3, 6, 7]
target = 7
print(combination_sum(candidates, target))
在这个例子中:
candidates
是一个不包含重复元素的整数数组。target
是目标和。backtrack
是递归函数,它尝试构建一个组合,使其和为 target
。remain
(剩余需要达到的目标和)变成 0,说明找到了一个有效的组合,将其添加到结果列表 result
中。remain
变成负数,说明当前路径不可能成功,因此回溯。candidates.sort()
对候选数字进行排序,可以避免选择相同的数字,有助于剪枝。result
存储所有找到的组合。回溯算法是一种强大的算法框架,可以解决许多涉及搜索的问题,尤其是在组合数学和约束满足问题中非常有用。
回溯算法是一种强大的问题解决方法,它通过探索所有可能的候选解来找到所有解或最优解。这种算法特别适合于解决组合问题、排列问题、划分问题、优化问题等。以下是回溯算法的一些主要应用领域:
组合问题:
排列问题:
划分问题:
优化问题:
游戏AI:
约束满足问题(CSP):
图论问题:
字符串问题:
调度问题:
网络设计:
逻辑推理:
生物信息学:
回溯算法的关键特点是它能够通过剪枝(即放弃那些不可能产生最优解的路径)来减少搜索空间,从而提高算法的效率。虽然回溯算法可能在最坏情况下需要指数级的时间复杂度,但在实际应用中,合理的剪枝策略可以显著减少计算量。
给定两个整数n和k,返回1.n中所有可能的k个数的组合
示例:输入:n=4, k=2 输出: [ [2,4],[3,4],[2,3],[1,2],[1,3],[1,4],]
from typing import List
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def backtrack(start,path):
if len(path) == k:
result.append(path[:])
for i in range(start,n+1):
path.append(i)
backtrack(i+1,path)
path.pop()
result = []
backtrack(1,[])
return result
找出所有相加之和为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]]:
def backtrack(remain,start,path):
if len(path) == k:
if remain == 0 :
result.append(path[:])
return
for i in range(start, 10):
if remain - i < 0:
break
path.append(i)
backtrack(remain-i,i+1,path)
path.pop()
result = []
backtrack(n,1,[])
return result
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
# 电话按键对应的字母
phone_map = {'2':'abc',
'3':'def',
'4':'ghi',
'5':'jkl',
'6':'mno',
'7':'pqrs',
'8':'tuv',
'9':'wxyz'}
result = []
if not digits:
return result
def backtrack(combination,next_digits):
if len(next_digits) == 0:
result.append(combination)
else:
for letter in phone_map[next_digits[0]]:
backtrack(combination + letter, next_digits[1:])
backtrack("",digits)
return result
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
from typing import List
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
result = []
def trackback(remain,combo,index):
if remain == 0:
result.append(list(combo))
return
if remain < 0:
return
for i in range(index,len(candidates)):
combo.append(candidates[i])
trackback(remain-candidates[i],combo,i)
combo.pop()
candidates.sort()
trackback(target,[],0)
return result
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]
from typing import List
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
result = []
candidates.sort() # 排序,以便跳过重复的数字
def backtrack(remain, combo, index):
if remain == 0:
result.append(list(combo)) # 找到一个解,添加到结果中
return
if remain < 0:
return # 超过目标值,剪枝
for i in range(index, len(candidates)):
# 跳过重复的数字
if i > index and candidates[i] == candidates[i - 1]:
continue
# 如果当前数字大于剩余需要的数字,剪枝
if candidates[i] > remain:
break
combo.append(candidates[i])
backtrack(remain - candidates[i], combo, i + 1) # i + 1 确保不使用同一元素
combo.pop() # 回溯,移除最后一个元素
backtrack(target, [], 0)
return result
# 输入示例
candidates = [10, 1, 2, 7, 6, 1, 5]
target = 8
solution = Solution()
print(solution.combinationSum2(candidates, target))
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]
def is_palindrome(s: str) -> bool:
# 将字符串转换为统一的小写或大写,以忽略大小写差异
s = s.lower()
# 反转字符串
reverse_s = s[::-1]
# 比较原始字符串和反转后的字符串
return s == reverse_s
# 测试
print(is_palindrome("madam")) # 输出: True
print(is_palindrome("hello")) # 输出: False
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “[email protected]” 是 无效的 IP 地址。
from typing import List
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def backtrack(start, path, s):
# 如果路径中的部分数量为4且已经处理完所有字符,则添加到结果中
if len(path) == 4 and start == len(s):
result.append(".".join(path))
return
# 尝试添加下一个部分到路径中
#从当前开始索引 start 开始,尝试长度为1到3的子字符串
for end in range(start + 1, min(start + 4, len(s) + 1)):
part = s[start:end]
# 检查部分是否有效
if (part[0] == '0' and len(part) > 1) or (int(part) > 255):
continue
backtrack(end, path + [part], s)
result = []
backtrack(0, [], s)
return result
# 示例
solution = Solution()
s = "25525511135"
print(solution.restoreIpAddresses(s))
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def backtrack(start, path):
# 将当前路径添加到结果中
result.append(list(path))
for i in range(start, len(nums)):
# 选择当前元素
path.append(nums[i])
# 递归选择下一个元素
backtrack(i + 1, path)
# 回溯,移除最后一个元素
path.pop()
result = []
backtrack(0, [])
return result
# 示例
solution = Solution()
nums = [1, 2, 3]
print(solution.subsets(nums))
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
from typing import List
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
def backtrack(start, path):
# 将当前路径添加到结果中
result.append(list(path))
for i in range(start, len(nums)):
# 跳过重复的元素
if i > start and nums[i] == nums[i - 1]:
continue
# 选择当前元素
path.append(nums[i])
# 递归选择下一个元素,注意下一次从i+1开始,因为i+1的元素可能与i的元素相同
backtrack(i + 1, path)
# 回溯,移除最后一个元素
path.pop()
# 对数组进行排序,以便后续去重
nums.sort()
result = []
backtrack(0, [])
return result
# 示例
solution = Solution()
nums = [1, 2, 2]
print(solution.subsetsWithDup(nums))
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
from typing import List
class Solution:
def findSubsequences(self, nums: List[int]) -> List[List[int]]:
def backtrack(start, path):
# 将当前路径添加到结果中,如果路径长度至少为2
if len(path) > 1:
result.append(list(path))
for i in range(start, len(nums)):
# 跳过重复的元素
if i > start and nums[i] == nums[i - 1]:
continue
# 如果当前元素大于前一个元素,不选择它
if path and nums[i] < path[-1]:
continue
# 选择当前元素
path.append(nums[i])
# 递归选择下一个元素,注意下一次从i+1开始
backtrack(i + 1, path)
# 回溯,移除最后一个元素
path.pop()
result = []
backtrack(0, [])
return result
# 示例
solution = Solution()
nums = [4, 6, 7, 7]
print(solution.findSubsequences(nums))
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def backtrack(first=0):
if first == len(nums):
# 找到一个全排列,添加到结果中
result.append(nums[:])
for i in range(first, len(nums)):
# 交换 first 和 i 位置上的元素
nums[first], nums[i] = nums[i], nums[first]
# 继续递归寻找排列
backtrack(first + 1)
# 回溯,恢复 first 和 i 位置上的元素
nums[first], nums[i] = nums[i], nums[first]
result = []
backtrack()
return result
# 示例
solution = Solution()
nums = [1, 2, 3]
print(solution.permute(nums))
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
class Solution:
# 主函数,接收一个整数数组nums,并返回所有唯一的排列组合
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
# 对输入数组进行排序,以便于后续去重
nums.sort()
# 初始化结果数组,用于存储所有排列
result = []
# 初始化路径数组,用于构建当前排列
# 初始化已使用数组,用于标记数组中的元素是否已经被使用
# 初始化为False,表示所有元素都未使用
self.backtrack(nums, [], [False]*len(nums), result)
# 返回结果数组
return result
# 回溯函数,用于生成所有排列
def backtrack(self, nums, path, used, result):
# 如果当前路径的长度等于原数组的长度,说明找到了一个完整的排列
if len(path) == len(nums):
# 将当前排列添加到结果数组中
result.append(path[:])
# 遍历数组中的每个元素
for i in range(len(nums)):
# 如果当前元素与前一个元素相同,并且前一个元素没有被使用过,或者当前元素已经被使用过,则跳过
# 这是为了避免重复的排列
if (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]) or used[i]:
continue
# 将当前元素标记为已使用
used[i] = True
# 将当前元素添加到路径中
path.append(nums[i])
# 递归调用回溯函数,处理下一个元素
self.backtrack(nums, path, used, result)
# 回溯,移除路径中的最后一个元素,恢复状态
path.pop()
# 将当前元素标记为未使用
used[i] = False
给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
# 初始化邻接表,用于存储图的邻接表表示
self.adj = {}
# 根据目的地对机票进行排序,确保在DFS中优先考虑字典序较小的目的地
tickets.sort(key=lambda x: x[1])
# 构建图的邻接表
for u, v in tickets:
if u in self.adj:
self.adj[u].append(v)
else:
self.adj[u] = [v]
# 初始化结果列表,用于存储最终的行程
self.result = []
# 从'JFK'开始深度优先搜索
self.dfs('JFK')
# 返回结果列表,由于路径是反向添加的,所以需要反转
return self.result[::-1]
def dfs(self, s):
# 只要当前节点有可到达的目的地,就继续搜索
while s in self.adj and len(self.adj[s]) > 0:
v = self.adj[s][0]
# 使用第一张机票,从当前目的地出发
self.adj[s].pop(0)
# 递归地探索下一个目的地
self.dfs(v)
# 如果当前节点没有可到达的目的地,将其添加到结果列表中
self.result.append(s)
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
# 初始化结果列表
self.result = []
# 初始化棋盘,None代表空,0代表皇后
self.board = [[None for _ in range(n)] for _ in range(n)]
# 记录列是否有皇后
self.cols = [False] * n
# 记录对角线是否有皇后
self.diags1 = [False] * (2 * n - 1)
# 记录反对角线是否有皇后
self.diags2 = [False] * (2 * n - 1)
# 开始回溯搜索
self.backtrack(0, n)
return self.result
def backtrack(self, row, n):
# 如果已经放置了n个皇后,记录解法
if row == n:
# 将棋盘转换为结果所需的格式
solution = [''.join(['Q' if cell is not None else '.' for cell in row]) for row in self.board]
self.result.append(solution)
return
for col in range(n):
if not self.cols[col] and not self.diags1[row - col + n - 1] and not self.diags2[row + col]:
# 放置皇后
self.board[row][col] = 'Q'
# 标记列、对角线和反对角线
self.cols[col] = True
self.diags1[row - col + n - 1] = True
self.diags2[row + col] = True
# 递归放置下一行的皇后
self.backtrack(row + 1, n)
# 回溯,移除皇后
self.board[row][col] = None
self.cols[col] = False
self.diags1[row - col + n - 1] = False
self.diags2[row + col] = False
# 测试代码
n = 4
sol = Solution()
solutions = sol.solveNQueens(n)
for solution in solutions:
for row in solution:
print(row)
print()
编写一个程序,通过填充空格来解决数独问题。
一个数独的解法需遵循如下规则: 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 ‘.’ 表示。
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
self.backtracking(board)
def backtracking(self, board: List[List[str]]) -> bool:
# 若有解,返回True;若无解,返回False
for i in range(len(board)): # 遍历行
for j in range(len(board[0])): # 遍历列
# 若空格内已有数字,跳过
if board[i][j] != '.': continue
for k in range(1, 10):
if self.is_valid(i, j, k, board):
board[i][j] = str(k)
if self.backtracking(board): return True
board[i][j] = '.'
# 若数字1-9都不能成功填入空格,返回False无解
return False
return True # 有解
def is_valid(self, row: int, col: int, val: int, board: List[List[str]]) -> bool:
# 判断同一行是否冲突
for i in range(9):
if board[row][i] == str(val):
return False
# 判断同一列是否冲突
for j in range(9):
if board[j][col] == str(val):
return False
# 判断同一九宫格是否有冲突
start_row = (row // 3) * 3
start_col = (col // 3) * 3
for i in range(start_row, start_row + 3):
for j in range(start_col, start_col + 3):
if board[i][j] == str(val):
return False
return True