算法基础——回溯
回溯和递归是同时存在的。回溯算法效率并不高,本质是穷举,用于某些只能暴力搜索解决的问题。
这些问题可以分为:
- 组合问题
- 排列问题
- 切割问题
- 子集问题
- 棋盘问题:N皇后,解数独
回溯解决问题可以抽象成为N叉树的树型结构
。回溯在循环中设计递归
,进行层叠嵌套。其中for循环横向遍历,递归纵向遍历,回溯不断在上一层调整结果集。
1、组合 leetcode 77
copy
:本题添加path时使用py内置copy函数进行浅拷贝。直接使用path是引用,会因之后的递归path改变而更改(尽管前面的path已经加入list)。而使用浅拷贝而不是深拷贝,是因为ans中的每个path中存放的是数字不会被更改。具体两者区别
list和array使用切片也有区别
全局变量与局部变量
:局部可以调用相对更全局的变量,而全局要调用局部需要global声明。
range
:如果在 range 函数中,第一个参数大于第二个参数,并且最后一个参数为 -1,那么表示生成一个递减的区间。
例如,range(5, 1, -1) 会生成一个递减的区间: [5, 4, 3, 2]。这个区间包括起始值 5,但不包括结束值 1,每次递减 1。
在设计循环、递归以及其构成的回溯,动态规划,贪心算法的时候,明白他们在重复相似行为时保持的是什么,非常关键。如循环不变式,再到本题的情况。
对于下方第一个算法,函数dfs传入的i表示当前考虑的数字范围,从整体思路上看,是依次考虑最大数取n,n-1,n-2···的情况。对于下方第二个算法,函数dfs传入的i表示现在考虑第i个数在和不在里面的情况。if i>d则i可以不在里面,这时候继续递归。然后最后三行考虑i在里面的情况。
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
ans=[]
path=[]
def dfs(i:int)->None:
d=k-len(path) #还需要选择d个数字
if d==0:
ans.append(path.copy())
return
for j in range(i,d-1,-1):# 这里逆向写更自然,当然你也可以翻转写它
path.append(j)
dfs(j-1)
path.pop()
dfs(n)
return ans
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
ans = []
path = []
def dfs(i: int) -> None:
d = k - len(path) # 还要选 d 个数
if d == 0:
ans.append(path.copy())
return
# 不选 i
if i > d: dfs(i - 1)
# 选 i
path.append(i)
dfs(i - 1)
path.pop()
dfs(n)
return ans
至于python自带的组合数库,也可一行解决本题
from itertools import combinations
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
return list(combinations(range(1, n+1), k))
2、组合总和Ⅲ leetcode 216
拿到题自己写了下,有点累赘
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
ans = []
path = []
def dfs(sum: int, t: int):
d=k-len(path)
if sum==0 and d==0:
ans.append(path.copy())
return
if sum==0 or d==0:
return
for i in range(min(sum,t-1,9),0,-1):
path.append(i)
dfs(sum-i,i)
path.pop()
dfs(n,n+1)# 寻找和为n,最大值小于n+1的满足K-len(path)限制的数字组合
return ans
下面是灵茶山艾府的做法,与上题一样,
法一:每次迭代以最大值不同区分。
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
ans = []
path = []
def dfs(i: int, t: int) -> None:
d = k - len(path) # 还要选 d 个数
if t < 0 or t > (i * 2 - d + 1) * d // 2: # 剪枝首项加尾项乘以项数除以二
return
if d == 0: # 找到一个合法组合
ans.append(path.copy())
return
for j in range(i, d - 1, -1):
path.append(j)
dfs(j - 1, t - j)
path.pop()
dfs(9, n)
return ans
法二:每次迭代以选不选这个数区分
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
ans = []
path = []
def dfs(i: int, t: int) -> None:
d = k - len(path) # 还要选 d 个数
if t < 0 or t > (i * 2 - d + 1) * d // 2: # 剪枝
return
if d == 0: # 找到一个合法组合
ans.append(path.copy())
return
# 不选 i
if i > d:
dfs(i - 1, t)
# 选 i
path.append(i)
dfs(i - 1, t - i)
path.pop()
dfs(9, n)
return ans
3、电话号码的字母组合 leetcode 17
自己开头写了一版,由于字典写成int型,复杂了一点,其实想到后面for循环直接循环的digits字符串,应该用字符串的数字。
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
dic={2:"abc", 3:"def", 4:"ghi", 5:"jkl",
6:"mno", 7:"pqrs", 8:"tuv", 9:"wxyz"}
ans=[]
d=[]
n=len(digits)-1
def dfs(n): # 获得n位置
# d=len(digits)-len(d)
if len(d)==len(digits):
ans.append("".join(d[::-1]))
return
for i in dic[int(digits[n])]:
d.append(i)
dfs(n-1)
d.pop()
dfs(n)
return ans
腐烂的橘子写的回溯传两个参数就非常简洁
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits: return []
phone = {'2':['a','b','c'],
'3':['d','e','f'],
'4':['g','h','i'],
'5':['j','k','l'],
'6':['m','n','o'],
'7':['p','q','r','s'],
'8':['t','u','v'],
'9':['w','x','y','z']}
def backtrack(conbination,nextdigit):
if len(nextdigit) == 0:
res.append(conbination)
else:
for letter in phone[nextdigit[0]]:
backtrack(conbination + letter,nextdigit[1:])# 字符串拼接操作值得学习
res = []
backtrack('',digits)
return res
实际上本题使用回溯可能比循环嵌套运行时间还久一点,主要是本题不存在去重,剪枝等情况,所以回溯也不具有思路简单的优势。
时间复杂度上
,O(3^m× 4^n ),其中m是输入中对应3 个字母的数字个数,n是输入中对应4个字母的数字个数。
空间复杂度上
,O(m+n),哈希表(字典)空间固定,递归层数占主导地位,取决于总的输入数字数目。
tips:写的时候遇到的一些问题
ans.append(“”.join(d.reverse())):d.reverse()返回的是None,而不是反转后的列表。应该先使用d.reverse()来对列表进行反转,然后再使用"".join(d)来将反转后的列表转换为字符串。这时可以用字符串的切片操作[::-1]来解决反转列表的需求。
总结
- 回溯算法中的循环,对应遍历树结构一层节点的横向进程,而递归相当于纵向进程
- 明白算法中设计的递归函数,循环结构的不变性很关键(完成什么样的相似的任务)
- 回溯法三部曲
1、递归函数的返回值以及参数
2、回溯函数终止条件
3、单层搜索的过程 - 对于回溯题目,有时可以进行减枝优化
- 提到组合想到回溯