目录
- 0、基础
- 什么是回溯?
- 回溯法解决的问题
- 回溯模板
- 1、组合问题
- 77. 组合
- 216.组合总和III
- 17. 电话号码的字母组合
- 39. 组合总和:
- 40.组合总和II
0、基础
什么是回溯?
回溯是一种穷举的搜索算法,并不是一个高效的算法,当一些问题暴力搜素也无法穷举的时候就要使用回溯。
回溯法解决的问题都可以抽象为树形结构
回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
本质上是for循环+递归
回溯法解决的问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
回溯模板
void backtracking(参数) {
if (终止条件) { // 搜索到了叶子结点
存放结果; // 子集、某种排列方式、某种切割方式
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
1、组合问题
77. 组合
给定两个整数 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 __init__(self):
self.path = [] # 存放路径
self.result = [] # 存放符合条件(k个数)的结果的二维数组
def backtracking(self, n, k, startindex): # 参数n:树的宽度、参数k:遍历的深度、startindex:记录本层递归中集合从哪里开始遍历
# 终止条件:到达递归深度:叶子节点:path已经收集到了k个元素
if len(self.path) == k:
self.result.append(self.path[:])
return
# 单层逻辑:for从starindex开始遍历,将结果加入path中,然后递归下一层,一直到找到叶子节点,然后返回答案,并撤销处理过程
for i in range(startindex, n+1):
self.path.append(i)
self.backtracking(n, k, i+1)
self.path.pop()
# 正常情况每一层[i,n]
# 但是要考虑到剩余元素不满足k的情况,n = 4 ,k = 4 ,i = 2, 剩余元素个数为n-i+1 = 3,现在记录的个数为len(path)
# n - i +1 + len(path)>= k -> i <= n - k + 1 +len(path)
# i \in [startindex,n - k + 1 +len(path)]
for i in range(startindex, n - (k - len(self.path)) + 2):
self.path.append(i)
self.backtracking(n,k,i+1)
self.path.pop()
def combine(self, n: int, k: int) -> List[List[int]]:
self.backtracking(n, k, 1)
return self.result
🌟在for i in range(startindex, n+1):
这里面的startindex保证的是树层去重(防止出现[1,2]和[2,1]的情况,组合问题特性 )
🌟在 self.backtracking(n, k, i+1)
:这里面的i+1
保证的是树枝去重(防止出现[1,1]的情况,也就是同一个元素取了两次,这是一个元素只能出现一次的特性 )
😙剪枝优化:当目前剩余可以选择的元素不够k的时候就没必要继续了,n - i +1 + len(path)>= k -> i <= n - k + 1 +len(path)
❤️为什么是path[:]不是path?
- 浅拷贝vs引用
- path[:] 是浅拷贝,之后对 self.path 的修改不会影响已经存储在 self.res 中的结果
- path是引用,如果 self.path 在之后的递归中被修改,那么 self.res 中的结果也会被修改,因为它们指向的是同一个列表对象。
216.组合总和III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
class Solution:
def __init__(self):
self.res = []
self.path = []
def backtracking(self, n , k, sum, startindex):
# 剪枝:当前sum已经超过n的节点就不需要继续遍历了
if sum > n:
return
# 终止条件:path的长度为k
if len(self.path) == k:
if sum == n: # 如果此时计算的path路径上的值的和为n,就是一种结果组合
self.res.append(self.path[:])
return
# for循环横向遍历,不能有重复的数
for i in range(startindex, 10):
sum += i
self.path.append(i)
self.backtracking(n, k, sum, i+1)
sum -= i
self.path.pop()
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
self.backtracking(n,k,0,1)
return self.res
🌟组合问题:for i in range(startindex,n)
🌟无重复元素:backtracking(n,k,i+1)
😙剪枝优化:
1️⃣ 当前总和已经超过n:if sum > n:
2️⃣ 当前剩余元素的数量加上已经记录的元素数量要保证超过k个:9-i+1 + len(path) >=k ➡️ i<=9 - (k - path.size()) + 1
17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
class Solution:
def __init__(self):
self.s = ""
self.result = []
self.lettermap = [
"", # 0
"", # 1
"abc", # 2
"def", # 3
"ghi", # 4
"jkl", # 5
"mno", # 6
"pqrs", # 7
"tuv", # 8
"wxyz" # 9
]
def backtracking(self, digits, index):
if index == len(digits):
self.result.append(self.s)
return
# 把当前处理的字符“2”变成数字2
digit = int(digits[index])
stringletter = self.lettermap[digit]
for letter in stringletter:
self.s += letter
self.backtracking(digits, index + 1)
self.s = self.s[:-1]
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
self.backtracking(digits, 0)
return self.result
39. 组合总和:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
class Solution:
def __init__(self):
self.path = []
self.res = []
self.sum = 0
def backtracking(self,candidates,target,startindex):
if self.sum > target:
return
if self.sum == target:
self.res.append(self.path[:])
return
for i in range(startindex,len(candidates)):
self.sum += candidates[i]
self.path.append(candidates[i])
self.backtracking(candidates,target,i)
self.sum -= candidates[i]
self.path.pop()
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
self.backtracking(candidates,target,0)
return self.res
🌟组合问题:for i in range(startindex,n)
🌟可以重复元素:backtracking(n,k,i)
40.组合总和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。
🔴数组中含有重复元素
🔴每个元素只能用一次➡️组合中同一元素不能重复
🔴组合问题
class Solution:
def backtracking(self, candidates, target, total, startIndex, used, path, result):
if total == target:
result.append(path[:])
return
for i in range(startIndex, len(candidates)):
# 对于相同的数字,只选择第一个未被使用的数字,跳过其他相同数字
if i > startIndex and candidates[i] == candidates[i - 1] and not used[i - 1]:
continue
if total + candidates[i] > target:
break
total += candidates[i]
path.append(candidates[i])
used[i] = True
self.backtracking(candidates, target, total, i + 1, used, path, result)
used[i] = False
total -= candidates[i]
path.pop()
def combinationSum2(self, candidates, target):
used = [False] * len(candidates)
result = []
candidates.sort()
self.backtracking(candidates, target, 0, 0, used, [], result)
return result
🌟组合问题:for i in range(startindex,n)
🌟无重复元素:backtracking(n,k,i+1)
☀️因为初始数组[1,1,2,3]
存在重复元素,有可能出现这样的情况,[1(0),2,3]
和[1(1),2,3]
、虽然满足无重复元素(原数组的不同元素),但是生成的新组合还是重复了,so,需要删除这种重复
if i > 0 and candidates[i] == candidates[i - 1] and used[i - 1] == false: