代码随想录训练营 Day22打卡 回溯算法part01
一、 理论基础
什么是回溯法
回溯法,又称回溯搜索法,是一种搜索方式,其本质是穷举所有可能的解并选择符合条件的解。
回溯法常常与递归联系在一起,递归是回溯的基础,回溯是递归的副产品。在回溯中,函数一般会通过递归调用自身来进行问题的求解。
回溯法的效率
尽管回溯法是一种非常重要的算法,但其效率并不高。回溯法的核心是穷举,即尝试所有可能的解,然后从中选择符合条件的解。为了提升效率,可以在回溯过程中加入剪枝操作,以减少不必要的计算。然而,回溯法的本质决定了它并不是一个高效的算法。
回溯法解决的问题
回溯法可以用于解决以下几类问题:
- 组合问题:从N个数中按一定规则找出k个数的集合。
- 切割问题:将一个字符串按一定规则进行切割的方式。
- 子集问题:求一个N个数的集合中符合条件的子集。
- 排列问题:求N个数按一定规则全排列的方式。
- 棋盘问题:如N皇后问题、数独问题等。
回溯法模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
这份模板对于解决回溯法的题目非常重要,通过理解和应用这个模板,可以解决许多复杂的问题。初学者可能会觉得有点抽象,但在具体题目讲解中会更容易理解。已经做过回溯法题目的同学,应该会对这个模板感同身受。
二、 力扣77. 组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 :
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
我把组合问题抽象为如下树形结构:
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度。
主要思路:
- 初始化和启动回溯:
我们从combine方法开始,初始化一个空的结果集result。
- 回溯函数 backtracking:
backtracking函数用于递归生成所有可能的组合。
- 递归终止条件:
当当前路径path的长度等于目标组合长度k时,我们找到一个有效的组合,将其复制并加入结果集result。
- 递归过程:
遍历从startIndex到n - (k - len(path)) + 2的范围内的所有可能选择。这里的范围通过优化减少不必要的遍历,提升效率。
对于每个选择:
将当前数字i加入当前路径path。
递归调用backtracking,并将startIndex更新为i + 1,继续生成后续的组合。
回溯:在递归返回后,撤销当前选择,即从path中移除最后一个数字。
- 优化:
循环的结束条件通过n - (k - len(path)) + 2进行了优化,以避免不必要的计算,提升效率。
from typing import List
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result = [] # 存放结果集
self.backtracking(n, k, 1, [], result)
return result
def backtracking(self, n: int, k: int, startIndex: int, path: List[int], result: List[List[int]]):
"""
使用回溯算法生成组合
:param n: 1到n的整数范围
:param k: 组合中数字的个数
:param startIndex: 当前递归的起始索引
:param path: 当前组合路径
:param result: 存放所有组合结果的列表
"""
if len(path) == k:
# 当路径长度等于k时,将当前路径加入结果集
result.append(path[:])
return
# 优化:如果剩余的数字已经不足以填满k个位置,则提前结束循环
for i in range(startIndex, n - (k - len(path)) + 2):
path.append(i) # 处理当前节点,将i加入路径
self.backtracking(n, k, i + 1, path, result) # 递归,更新起始索引为i+1
path.pop() # 回溯,撤销处理的节点,移除最后一个元素
# 示例调用
sol = Solution()
print(sol.combine(4, 2)) # 输出: [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
三、 力扣216. 组合总和III
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 :
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
本题k相当于树的深度,9(因为整个集合就是9个数)就是树的宽度。
例如 k = 2,n = 4的话,就是在集合[1,2,3,4,5,6,7,8,9]中求 k(个数) = 2, n(和) = 4的组合。
选取过程如图:
图中,可以看出,只有最后取到集合(1,3)和为4 符合条件。
代码思路:
- 初始化和启动回溯:
在 combinationSum3 方法中,初始化一个空的结果集 result。
调用 backtracking 方法开始回溯过程。
- 回溯函数 backtracking:
如果当前和 currentSum 超过目标和 targetSum,则提前返回(剪枝)。
如果路径长度等于 k 且当前和等于 targetSum,将当前路径加入结果集。
否则,从当前起始索引遍历到 9,依次尝试每个数字。
from typing import List
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
"""
找到所有由 k 个不同的数字组成的组合,这些数字的和为 n
"""
result = [] # 存放结果集
self.backtracking(n, k, 0, 1, [], result)
return result
def backtracking(self, targetSum: int, k: int, currentSum: int, startIndex: int, path: List[int], result: List[List[int]]):
"""
使用回溯算法生成组合
:param targetSum: 目标和
:param k: 组合中数字的个数
:param currentSum: 当前路径的和
:param startIndex: 当前递归的起始索引
:param path: 当前组合路径
:param result: 存放所有组合结果的列表
"""
if currentSum > targetSum: # 剪枝操作
return # 如果当前和超过目标和,则返回
if len(path) == k:
# 当路径长度等于k时,检查当前和是否等于目标和
if currentSum == targetSum:
result.append(path[:]) # 将当前路径加入结果集
return
# 优化:如果剩余的数字已经不足以填满k个位置,则提前结束循环
for i in range(startIndex, 9 - (k - len(path)) + 2):
currentSum += i # 处理当前节点,将i加入当前和
path.append(i) # 将i加入路径
self.backtracking(targetSum, k, currentSum, i + 1, path, result) # 递归,更新起始索引为i+1
currentSum -= i # 回溯,撤销处理的节点,从当前和中减去i
path.pop() # 回溯,从路径中移除最后一个元素
# 示例调用
sol = Solution()
print(sol.combinationSum3(3, 7)) # 输出: [[1, 2, 4]]
print(sol.combinationSum3(3, 9)) # 输出: [[1, 2, 6], [1, 3, 5], [2, 3, 4]]
四、 力扣17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 :
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
每个按键(2-9)对应一组字母,给定一串数字,生成所有可能的字母组合。通过回溯算法实现这一目标。
版本一
- 初始化:
初始化 letterMap,包含每个数字对应的字母。
初始化 result 作为存放结果的列表。
初始化 s 作为当前组合的字符串。
- 回溯函数 backtracking:
如果当前索引等于输入数字的长度,表示已完成一个组合,将其加入结果集。
获取当前数字对应的字母集,并依次尝试每个字母。
将字母加入当前组合 s,递归处理下一个数字。
回溯时删除最后加入的字母,恢复状态。
- 主函数 letterCombinations:
检查输入是否为空,为空则返回空结果。
调用回溯函数从第一个数字开始处理。
class Solution:
def __init__(self):
self.letterMap = [
"", # 0
"", # 1
"abc", # 2
"def", # 3
"ghi", # 4
"jkl", # 5
"mno", # 6
"pqrs", # 7
"tuv", # 8
"wxyz" # 9
]
self.result = []
self.s = ""
def backtracking(self, digits, index):
if index == len(digits):
self.result.append(self.s)
return
digit = int(digits[index]) # 将索引处的数字转换为整数
letters = self.letterMap[digit] # 获取对应的字符集
for i in range(len(letters)):
self.s += letters[i] # 处理字符
self.backtracking(digits, index + 1) # 递归调用,注意索引加1,处理下一个数字
self.s = self.s[:-1] # 回溯,删除最后添加的字符
def letterCombinations(self, digits):
if len(digits) == 0:
return self.result
self.backtracking(digits, 0)
return self.result
版本二
- 初始化:
初始化 letterMap,包含每个数字对应的字母。
初始化 result 作为存放结果的列表。
- 回溯函数 getCombinations:
如果当前索引等于输入数字的长度,表示已完成一个组合,将其加入结果集。
获取当前数字对应的字母集,并依次尝试每个字母。
将字母加入当前组合 s,递归处理下一个数字。
- 主函数 letterCombinations:
检查输入是否为空,为空则返回空结果。
调用回溯函数从第一个数字开始处理。
class Solution:
def __init__(self):
self.letterMap = [
"", # 0
"", # 1
"abc", # 2
"def", # 3
"ghi", # 4
"jkl", # 5
"mno", # 6
"pqrs", # 7
"tuv", # 8
"wxyz" # 9
]
self.result = []
def getCombinations(self, digits, index, s):
if index == len(digits):
self.result.append(s)
return
digit = int(digits[index]) # 将索引处的数字转换为整数
letters = self.letterMap[digit] # 获取对应的字符集
for letter in letters:
self.getCombinations(digits, index + 1, s + letter) # 递归调用,注意索引加1,处理下一个数字
def letterCombinations(self, digits):
if len(digits) == 0:
return self.result
self.getCombinations(digits, 0, "")
return self.result