代码随想录系列文章目录
回溯篇 - 组合
39.组合总和
题目链接
这道题和216.组合总和3的区别是,可以重复选取当前元素
还是要画递归树的图,虽然说可以重复选,但是组合还是一个集合问题,[2,2,3] 和 [3,2,2]还是同一种答案。这说明了,在每层的递归逻辑里,还是需要一个start, 去控制每层遍历的起点。
那么重复选取当前元素,怎么实现呢?
下一层递归可以从上一层已经遍历到的地方开始就好了。
这个树的宽度就是candidate数组的宽度毋庸置疑,但是深度是不定的,组合问题就是收割叶子结点,出口就是sum(path) >= target
剪枝优化:不进入下一层递归
对于上面这个图表示的dfs过程, 对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。
其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。
那么可以在for循环的搜索范围上做做文章了。
对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。
就是这个样子的:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
n = len(candidates)
res = []
def rec(start, cnt, path):
if cnt >= target:
if cnt == target:
res.append(path)
return
for i in range(start, n):
if cnt + candidates[i] > target:
return
rec(i, cnt+candidates[i], path+[candidates[i]])
rec(0, 0, [])
return res
这里有个坑,也是这一轮刷题没解决的问题,res如果写在递归函数的参数中,代码会有一些例子过不了,写在全局变量,然后让递归函数去更新它的值就可以。希望能得解释
40.组合总和II
题目链接
这道题的难点在于,和39相比,集合(数组candidates)有重复元素,但还不能有重复的组合,所以需要去重。
但是 最重要的一点是,这个去重是在同一层上的(每层递归的逻辑上),同一树枝上的都是一个组合里的元素,不用去重
所以最重要的还是画出递归树:
所以我很容易就想出了,对数组排序,遍历数组的时候如果前面的和后面的值相同我们就continue.
但是到底是if candidate[i+1] == candidate[i]: 还是if candidate[i] == candidate[i-1]:
如果说是candidate[i+1]的话,数组会越界,因为i 是 (start, len(candidate))的,
所以说,在每层遍历的逻辑里, if candidate[i] == candidate[i-1]: 我们continue。但是要注意,i > start,因为i是每次从start开始的,这个start在下一层会右移一位避免重复,i-1必须大于等于start才行。
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
n = len(candidates)
res = []
def rec(start, cnt, path):
if cnt >= target:
if cnt == target:
res.append(path)
return
for i in range(start, n):
if i > start and candidates[i] == candidates[i-1]: continue #每一层中去重
if cnt + candidates[i] > target: return
rec(i+1, cnt+candidates[i], path+[candidates[i]])
rec(0, 0, [])
return res
131.分割回文串(第二次自己写写不出)
题目链接
分割问题其实是一种组合问题,但是比较抽象
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段…。
这道题我之前刷过一遍,但是再次写的时候发现还是不会,递归树画不出来
所以在这个题解里我把递归的逻辑明明白白写一遍吧
1.函数的参数
切割问题说了是一种组合问题,因此,参数中肯定有一个start, path装每一条路径的切割结果,也必不可少
2.递归的出口
切割问题是一种组合问题,组合问题收集的就是叶子结点。我觉得切割问题抽象的地方,**不同于普通组合问题的点,**也在于这,叶子结点是什么?也就是出口在哪
从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止终止条件。
那么在代码里什么是切割线呢?
在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。
if start >= len(s):
res.append
return
3.单层递归的逻辑
在单层递归的逻辑里,如何截取子串呢?
在for (int i = startIndex; i < s.size(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。
首先判断这个子串是不是回文,如果是回文,就加入在 path中,path用来记录切割过的回文子串。如果不是, continue
1我们可以用切片表示子串 temp = [start: i]
2我们可以用切片判断是不是回文(写一个函数,双指针判断也行)
if temp == temp[::-1]
这就是实现的递归树
class Solution:
def partition(self, s: str) -> List[List[str]]:
res = []
def rec(start, path):
if start >= len(s):
res.append(path)
return
for i in range(start, len(s)):
sub_s = s[start:i+1]
if sub_s == sub_s[::-1]:
rec(i+1, path+[sub_s])
else:
continue
rec(0, [])
return res
93.复原IP地址
这道题,尽管昨天写了131.分割回文串,今天还是感觉碰见这题不是很会下手写
在这里我凭感觉说出我脑子里怕的东西:1.是我不知道出口怎么写,也就是说,这个串什么时候分割完呢,还得加‘.’之类的,好恶心 2. 每一层的遍历怎么搞,还得判断一小段子串是不是合法的
说完最直观的感受了,我还是来详细的写一遍这道题的递归逻辑吧
1.参数,卡哥版的答案里,参数和出口用的是start 和 pointnum表示加的点,如果加够三个点, 最后一段也合法就把这一种结果加进res 。
但是我选择了模拟的写法,也就是不真的加’.', 而是寻找每一小块合法的子串,把他们加进path, 用path + [substr]就好了。
那么,参数就是 rec(start, path).切割问题类似组合问题, startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。
2.出口,
如果按着我的参数,组合问题切割问题,还是收集叶子结点,那么当len(path) == 4, 并且start > len()-1了, 我们把它加进res就好了。为什么要start > len() -1呢,因为我们要把整个串分割还原回去; 因为我们把每个substr加进path的操作都是下一层的rec(i+1, path+[]), 所以,我们的切割线start, 要大于最后一位,没有等号,才是遍历完最后一位了,保证结果含有整个串
3.单层递归逻辑
我们的子串还是s[start:i+1], 我们一个个i去遍历去试,如果substr合法了我们把它加进path,satrt从 i+1开始继续, 因为我们不能重复切割
for i in range(start,len(s)):
temp = s[start:i+1]
if isvalid(temp):
rec(i+1,path+[temp])
判断子串是否合法的逻辑:
只有两种情况是合法的,第一种位数为1的话,它要在0-9之间,第二种位数大于1的话,它要在10-255之间,而且sub[0] != 0, 其它的都不对
if len(s) > 1 and 10 <= int(s) <= 255 and s[0]!='0':
return True
elif len(s) == 1 and 0 <= int(s) <= 9:
return True
else: return False
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
res = []
def isvalid(s):
if len(s) > 1 and 10 <= int(s) <= 255 and s[0]!='0':
return True
elif len(s) == 1 and 0 <= int(s) <= 9:
return True
else: return False
def rec(start, path):
if len(path) == 4:
if start > len(s) - 1:
res.append('.'.join(path))
return
for i in range(start, len(s)):
substr = s[start: i+1]
if isvalid(substr):
rec(i+1, path+[substr])
rec(0, [])
return res
总结 131 和 93 与传统组合不同之处
最重要的切割和组合的不同,是我们不会画切割的递归树,其实切割也是一位位的去切割去试,只是说我们要判一个substr, 这个substr是 start 到 i 的。我们在这道题里去判断substr是不是合法ip的子串,在上道题里去判断这个substr是不是回文的 ,其实原理都一样,都是一位一位的去试出来的这个子串。如果一小段子串合法,我们往深去dfs, 递归调用回溯函数,把substr加进path, 上一层切割完一次子串,不能回头,start要从i+1开始,再去递归。