代码随想录算法训练营第26天 | 39.组合总和 40.组合总和II 131.分割回文串 93.复原IP地址

代码随想录系列文章目录

回溯篇 - 组合



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开始,再去递归。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值