写代码的第十九天
进入回溯啦!!
理论基础
代码随想录写的已经很详细了。
纯暴力搜索但是用for解决不了的问题,组合,排列,分割,子集,棋盘等问题。基本上所有的回溯问题都可以变成一个n叉树,都能画图画出来。
模版:
1、回溯的参数和返回值?具体问题具体分析。
2、回溯的终止条件?看上面说的几种问题的例子,其实会发现一般都是和叶子结点有关的。
if (终止条件):
存放结果
return
3、回溯搜索的遍历过程?回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小))
处理节点
backtracking(路径,选择列表) // 递归
回溯,撤销处理结果
77. 组合
思路
这种问题很像高中的数学题,画图很好理解,但是换成代码就不太容易,画一颗这样的树,其实我们要模拟的就是图中箭头的过程。
解决问题1:参数和返回值是什么?参数肯定有k和n,以及一个startindex,因为在图中我们已经标注出了从左向右取数,取过的数不在重复取,所以每次从哪个数开始取需要一个起始索引,也就是startindex。返回值应该是满足条件的所以结果集合,所以为了方便可以把这个满足条件的结果集合设置为全局变量,然后每次对结果做的更改都在全局变量中修改,所以设置了两个全局变量,一个是存储路径的path(通过上面的图可以发现最后的结果就是每条路径),一个是存储结果集也就是所有path的result。
解决问题2:终止条件是什么?类似于查找路径,所以回想二叉树找路径的终止条件,也就是到达叶子结点,但是本题我们是以树的形式在做并不是树,所以要考虑怎么才能替换一下呢?我们会看见题中要求的是k个数的组合,所以当路径长度为k的时候终止,那么也就是当path的长度等于k的时候终止。
解决问题3:单层搜索过程?其实就是进行树的路径的遍历,只不过这一次的每个结点的数值不是单纯的一个数,而是好多数,每个数都有满足条件的可能,所以要在原本二叉树的情况下对结点进行for循环,为了使用结点中的所有数值(横向用for循环,纵向用递归+回溯),这个时候就要用到startindex了,这个值代表着每个结点的起始数值。首先讲遍历到的结点append进path,也就是将当前处理的结点append进path,然后按照二叉树时候的逻辑开始递归该结点的子树,当处理完该结点所有的子树的时候,就证明这个结点的所有情况已经完成了,那么就需要将这个结点pop出去,让新的结点值进来,再次进行递归,直到所有的结点遍历完毕。
错误第一版:题中给的范围是[1,n],而我这个代码是[0,n-1]。
class Solution:
def __init__(self):
self.path = []
self.result = []
def combine(self, n: int, k: int) -> List[List[int]]:
self.backtracking(n,k,0)
return self.result
def backtracking(self,n,k,startindex):
if len(self.path) == k:
self.result.append(self.path.copy())
return
for i in range(startindex,n):
self.path.append(i)
self.backtracking(n,k,i)
self.path.pop()
错误第二版:输出结果包含[1,1][2,2][3,3][4,4]这种情况。错的地方在这self.backtracking(n,k,i)递归调用的时候startindex还是i??那不就重复用一个startindex了嘛!!!!!
class Solution:
def __init__(self):
self.path = []
self.result = []
def combine(self, n: int, k: int) -> List[List[int]]:
self.backtracking(n,k,1)
return self.result
def backtracking(self,n,k,startindex):
if len(self.path) == k:
self.result.append(self.path.copy())
return
for i in range(startindex,n+1):
self.path.append(i)
self.backtracking(n,k,i)
self.path.pop()
正确代码:
class Solution:
def __init__(self):
self.path = []
self.result = []
def combine(self, n: int, k: int) -> List[List[int]]:
self.backtracking(n,k,1)
return self.result
def backtracking(self,n,k,startindex):
if len(self.path) == k:
self.result.append(self.path.copy())
return
for i in range(startindex,n+1):
self.path.append(i)
self.backtracking(n,k,i+1)
self.path.pop()
思路(剪枝)
哈哈哈哈我本人根本没想到要剪枝笑死,我的脑子是个装饰品。
看了卡哥的视频,下面图的例子确实是没必要每一条路径都遍历的,所以需要剪枝。
在有需要剪枝操作的题中,需要考虑的就是这个for循环的范围是不是过大,本题原始的for的范围是startindex到n+1,但是从上面这个图可以看出是不需要到n+1的。当前的已经已经选择的元素个数,也就是len(path),还需要k-len(path)个结点,如果当前剩下的数字中都没有这么多结点了就可以直接剪枝了,完全不需要在继续遍历查找了,但是这样写是在for里面加一个判断条件,没那么简洁,所以我们要思考如何在for循环这个范围中做文章,也就是要让startindex的只能在有限的范围中进行循环遍历,超过这个循环直接结束,所以[startindex,n]的长度要最小是k,所以startindex的最大值就是n - (k - len(path)) + 1(这个式子带个数进去就明白了)。
正确代码:
class Solution:
def __init__(self):
self.path = []
self.result = []
def combine(self, n: int, k: int) -> List[List[int]]:
self.backtracking(n,k,1)
return self.result
def backtracking(self,n,k,startindex):
if len(self.path) == k:
self.result.append(self.path.copy())
return
for i in range(startindex,n-(k-len(self.path))+2):
self.path.append(i)
self.backtracking(n,k,i+1)
self.path.pop()
216.组合总和III
思路
这个题的思路和上一个题是一样的,只不过在每个结点的判断时加了一个判断sum和是否和target相等。
注意:
1、self.result.append(self.path.copy())这里面一定是copy,如果直接append的是path,这个path会随着后面的改变而改变的。
2、在下面的for循环中之前的题只要append值pop值就可以了,在这个题中因为我们要判断sum值是否和target值相等,所以需要计算sum值,那么在回溯到上一步的时候就需要让sum也回溯回去,所以需要-i操作。
正确代码:
class Solution:
def __init__(self):
self.path = []
self.result = []
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
self.backtracing(k,n,0,1)
return self.result
def backtracing(self,k,n,sums,startindex):
if len(self.path) == k:
if sums == n:
self.result.append(self.path.copy())
for i in range(startindex,10):
sums += i
self.path.append(i)
self.backtracing(k,n,sums,i+1)
sums -= i
self.path.pop()
思路(剪枝)
和上面的剪枝一样,但是要多一个,如果说当前的sums值已经大于target了,那就证明之后的遍历都不需要了。
正确代码:
class Solution:
def __init__(self):
self.path = []
self.result = []
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
self.backtracing(k,n,0,1)
return self.result
def backtracing(self,k,n,sums,startindex):
if sums > n:
return
if len(self.path) == k:
if sums == n:
self.result.append(self.path.copy())
for i in range(startindex,11-(k-len(self.path))):
sums += i
self.path.append(i)
self.backtracing(k,n,sums,i+1)
sums -= i
self.path.pop()
17.电话号码的字母组合
思路
最开始看见23的时候还想着两层for循环咔咔解决,然后看见这是回溯章节想到了要是123243489759这种难道要一直for吗,不合适不合适。
画图太重要了!!!!!我笔画的丑,用卡哥的记录一下。
解决问题1:参数和返回值是什么?参数肯定有digits,输入的字符串,以及一个index,这里和上面题不一样的地方在于,上面的是不能取之前重复取过的,所以每次都有一个新的开始,但是这个题我们看上面的图,第一层是2对应的字符,第二层是3对应的字符,2和3对应的字符本身就不是重复的,所以每次都从最原始开始遍历就可以了。index的作用是遍历原始输入字符串的长度,也就是图中的每一层。
解决问题2:终止条件是什么?和上面一样类似于二叉树中的查找路径,所以回想二叉树找路径的终止条件,也就是到达叶子结点,但是本题我们是以树的形式在做并不是树,所以要考虑怎么才能替换一下呢?也就是把输入的字符串的多长度遍历完就到达了树的叶子结点的那一层,此时终止。
解决问题3:单层搜索过程?从上面那个图可以看出横向for,纵向递归。
错误版本一:字符串在python中是不可修改的!!!!!!!没有什么append。
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 letterCombinations(self, digits: str) -> List[str]:
self.backtracing(digits,1)
return self.result
def backtracing(self,digits,index):
if index == len(digits):
self.result.append(self.s)
digit = int(digits[index])
for i in self.letterMap[digit]:
self.s.append(i)
self.backtracing(digits,index)
self.s.pop()
错误第二版:超出时间限制。没有正确更新index的值,导致陷入无限循环。每次递归调用时,应该将index增加1,以便在下一次迭代中处理下一个数字
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 letterCombinations(self, digits: str) -> List[str]:
self.backtracing(digits,1)
return self.result
def backtracing(self,digits,index):
if index == len(digits):
self.result.append(self.s)
digit = int(digits[index])
letter = self.letterMap[digit]
for i in range(len(letter)):
self.s += letter[i]
self.backtracing(digits,index)
self.s = self.s[:-1]
错误第三版:index out of range.index是从0开始的,因为输入的字符串下标是0开始。if index == len(digits):self.result.append(self.s)这句话之后必须要加return,返回值,因为当index增加到len(digits)时,应该立即返回而不是继续执行后续代码!!!!!
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 letterCombinations(self, digits: str) -> List[str]:
self.backtracing(digits,1)
return self.result
def backtracing(self,digits,index):
if index == len(digits):
self.result.append(self.s)
digit = int(digits[index])
letter = self.letterMap[digit]
for i in range(len(letter)):
self.s += letter[i]
self.backtracing(digits,index+1)
self.s = self.s[:-1]
错误第四版:下面这个测试用例不对,应该直接输出空,所以加一个判断,当digits为空的时候直接输出self.result就行。
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 letterCombinations(self, digits: str) -> List[str]:
self.backtracing(digits,0)
return self.result
def backtracing(self,digits,index):
if index == len(digits):
self.result.append(self.s)
return
digit = int(digits[index])
letter = self.letterMap[digit]
for i in range(len(letter)):
self.s += letter[i]
self.backtracing(digits,index+1)
self.s = self.s[:-1]
正确代码:
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 letterCombinations(self, digits: str) -> List[str]:
if len(digits) == 0:
return self.result
self.backtracing(digits,0)
return self.result
def backtracing(self,digits,index):
if index == len(digits):
self.result.append(self.s)
return
digit = int(digits[index])
letter = self.letterMap[digit]
for i in range(len(letter)):
self.s += letter[i]
self.backtracing(digits,index+1)
self.s = self.s[:-1]