代码随想录算法训练营第30天|回溯算法part03| 39. 组合总和 、40.组合总和II 、131.分割回文串
39. 组合总和
自己做
思路:
本身没有什么思路,但是按照卡哥的套路进行画树,慢慢的就知道了其中的归路!
有两个问题:
- 什么时候终止递归
- 对于重复的怎么处理,比如递归时会出现2,2,3和3,2,2这样的情况,仅需要一条路径就可以
手写画图如下:
代码:
python
class Solution(object):
def __init__(self):
self.result = []
self.path = []
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
self.backtracking(candidates, target, 0, 0)
return self.result
def backtracking(self, candidates, target, curSum, startIndex):
if curSum == target:
self.result.append(list(self.path))
return
elif curSum > target:
return
for index in range(startIndex, len(candidates)):
curSum += candidates[index]
self.path.append(candidates[index])
self.backtracking(candidates, target, curSum, index)
self.path.pop()
curSum -= candidates[index]
return
代码随想录
思路:
思路是一样的
剪支:
其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。
40. 组合总和 II
自己做
不会去除重复的组合
代码随想录
思路:
本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合。
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
强调一下,树层去重的话,需要对数组排序!
回溯三部曲
- 递归函数参数
此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。
这个集合去重的重任就是used来完成的。
def backtracking(self, candidates, target, curSum, startIndex, used):
- 递归终止条件
if curSum == target:
self.result.append(list(self.path))
return
elif curSum > target:
return
sum > target 这个条件其实可以省略,因为在递归单层遍历的时候,使用剪枝的操作就不用了,没用剪支就得加上
- 单层搜索的逻辑
这里与39.组合总和 (opens new window)最大的不同就是要去重了。
前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
此时for循环里就应该做continue的操作。
这块比较抽象,如图:
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
used[i - 1] == false,说明同一树层candidates[i - 1]使用过
可能有的录友想,为什么 used[i - 1] == false 就是同一树层呢,因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。
而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示:
代码:
python(自己写的)
class Solution(object):
def __init__(self):
self.result = []
self.path = []
def combinationSum2(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
candidates = sorted(candidates)
#print(candidates)
used = [0]*len(candidates)
self.backtracking(candidates, target, 0, 0, used)
return self.result
def backtracking(self, candidates, target, curSum, startIndex, used): # 1.
# 2. 这里没用剪支,所以得加上curSum > target:
if curSum == target:
self.result.append(list(self.path))
return
elif curSum > target:
return
# 3.
for index in range(startIndex, len(candidates)):
if index > 0 and candidates[index] == candidates[index-1] and used[index-1] == 0:
continue
used[index] = 1
curSum += candidates[index]
self.path.append(candidates[index])
self.backtracking(candidates, target, curSum, index+1, used)
self.path.pop()
curSum -= candidates[index]
used[index] = 0
return
131. 分割回文串
自己做
做不出来
代码随想录
思路:
本题这涉及到两个关键问题:
- 切割问题,有不同的切割方式
- 判断回文
其实切割问题类似组合问题
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
startIndex即可作为切割线
所以切割问题,也可以抽象为一棵树形结构,如图:
在切割的过程中,一旦发现不是回文直接返回,如果是一直是回文,那就直到切割到最后就返回其组合
树的宽度可以表示每条切割线的位置,树的深度表示切割线的条数
回溯三部曲
- 递归函数参数和返回值
def backtracking(self, s, startIndex)
- 递归函数终止条件
if startIndex == len(s): self.result.append(list(self.path))
从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。
- 单层搜索的逻辑
在for (int i = startIndex; i < s.size(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。
首先判断这个子串是不是回文,如果是回文,就加入在vector path中,path用来记录切割过的回文子串。
for index in range(startIndex, len(s)):
# [startIndex:index]为子串
if self.isPartition(s, startIndex, index):
self.path.append(s[startIndex:index+1])
else:
continue
self.backtracking(s, index+1)
self.path.pop()
return
注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
代码:
python
class Solution(object):
def __init__(self):
self.result = []
self.path = []
def partition(self, s):
"""
:type s: str
:rtype: List[List[str]]
"""
self.backtracking(s, 0)
return self.result
def backtracking(self, s, startIndex):# 1.
# 2.
if startIndex == len(s):
self.result.append(list(self.path))
# 3.
for index in range(startIndex, len(s)):
# [startIndex:index]为子串
if self.isPartition(s, startIndex, index):
self.path.append(s[startIndex:index+1])
else:
continue
self.backtracking(s, index+1)
self.path.pop()
return
def isPartition(self, s, startIndex, index):
left = startIndex
right = index
while left < right:
if s[left] == s[right]:
left += 1
right -= 1
else:
return False
return True