代码随想录算法训练营第二十九天|39. 组合总和、40.组合总和II、131.分割回文串

39. 组合总和

文档讲解代码随想录

题目链接:. - 力扣(LeetCode)

这道题目的关键点: 

candidates :无重复元素的数组、candidates 中的数字可以无限制重复被选取

与之前做过的组合问题的区别:

组合问题是给一个集合,找出k个元素的组合,把这些组合都列出来,递归的层数(树的深度是确定的)

这里没有限制组合里面的数量,找到了和为target的就可以,通过和来限定树的深度

这道题目的树形结构:

 这道题目中说元素可以重复使用,这就告诉我们如果这次选了2,下次还可以选2,也就是剩余的集合里要包括本次选取的一个元素。如果选取了5,那么下次就从[5,3]中选取,为什么不从[2,5,3]选取,因为在选2的时候已经会从[2,5,3]中选了,会有重复的情况

回溯法

回溯函数参数返回值:

这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入)

首先是题目中给出的参数,集合candidates, 和目标值target。

此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,依然用了sum。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合(opens new window)

注意以上只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面在讲解排列的时候会重点介绍

回溯函数终止条件

终止只有两种情况,sum大于target和sum等于target,sum等于target的时候,需要收集结果

(递归的过程就是纵向遍历,在增加sum的过程)

回溯搜索的单层搜索逻辑

 单层for循环依然是从startIndex开始,搜索candidates集合。注意本题和77.组合 (opens new window)、216.组合总和III (opens new window)的一个区别是:本题元素为可重复选取的,关键点就在于递归调用函数时,传入的statindex和当前读取到的数一样

class Solution:
    def __init__(self):
        # 初始化结果列表,用于存储最终的组合结果
        self.result = []
        
        # 初始化路径列表,用于存储当前组合的路径
        self.path = []
    
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        # 定义回溯函数
        def backtracking(candidates, current_sum, start_index):
            # 如果当前和等于目标值,将当前路径添加到结果列表中
            if current_sum == target:
                self.result.append(copy.deepcopy(self.path))  # 深拷贝当前路径
                return
            # 如果当前和超过目标值,停止继续递归
            elif current_sum > target:
                return 
            else:
                # 遍历候选数组,从start_index开始,避免重复组合
                for i in range(start_index, len(candidates)):
                    # 添加当前候选数到路径中
                    self.path.append(candidates[i])
                    # 更新当前和
                    current_sum += candidates[i]
                    # 递归调用回溯函数,!!关键点:注意传入的起始索引是i,允许重复使用当前数
                    backtracking(candidates, current_sum, i)
                    # 回溯,移除路径中的最后一个数
                    self.path.pop()
                    # 恢复当前和
                    current_sum -= candidates[i]

            return self.result
        
        # 调用回溯函数,初始和为0,起始索引为0
        return backtracking(candidates, 0, 0)

剪枝

在求和问题中,排序之后加剪枝是常见的套路!

具体的之后再看

40.组合总和II

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

题目关键点:candidates 中包含重复的元素 、candidates 中的每个数字在每个组合中只能使用一次、解集不能包含重复的组合。

这个题目的难点:集合(数组candidates)有重复元素,但还不能有重复的组合。如果 candidates = [2,5,2,1,2]  target = 5,那么按照之前的做法就会出现重复的组合,比如取到第一个2时,后面可以再取2和1,在后面在取到第二个2时,还可以再取到2和1

去重逻辑

假设排序后的集合:[1,1,2,3]

①假设取第一个1,后面就会从[1,2,3]里面选

②假设取第二个1,后面就会从[2,3]里面选

第①次选的剩下的元素不仅包含了第②次选的剩下的所有元素,而且1也包含了,那么注定②以1为开头的元素一定和①中以1为开头的选的元素是重复的

排序就是为了让相邻的元素放在一起,那么①中取1之后,②中再以1开头的都会包含在①中了

因为元素已经排序过了,②中的1后面的元素一定在①中的1后面

所以说去重的关键在于树层去重

代码实现

回溯三部曲

  • 递归函数参数

与39.组合总和 (opens new window)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。

这个集合去重的重任就是used来完成的。

  • 递归终止条件

与39.组合总和 (opens new window)相同,终止条件为 sum > target 和 sum == target

  • 单层搜索的逻辑

这里与39.组合总和 (opens new window)最大的不同就是要去重了。

前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]

此时for循环里就应该做continue的操作。

这块比较抽象,如图:

使用 used
class Solution:
    def __init__(self):
        self.path = []
        self.result = []
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        def backtracking(candidates,target,start_index,sum,used):
            if sum == target:
                self.result.append(copy.deepcopy(self.path))
                return
            elif sum > target:
                return
            else:
                for i in range(start_index,len(candidates)):
                    if i > 0:#可以这样写
                    # if i > start_index:#也可以这样写
                        if (candidates[i] == candidates [i-1] and  used[i-1] != True):#保证是树层去重
                            continue
                    self.path.append(candidates[i])
                    sum += candidates[i]
                    used[i] = True
                    backtracking(candidates,target,i+1,sum,used)
                    used[i] = False
                    self.path.pop()
                    sum -= candidates[i]
            return self.result
        used =  [False] * len(candidates)
        candidates.sort()
        print(candidates)
        return backtracking(candidates,target,0,0,used)
不使用used
class Solution:
    def __init__(self):
        self.path = []
        self.result = []
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        def backtracking(candidates,target,start_index,sum):
            if sum == target:
                self.result.append(copy.deepcopy(self.path))
                return
            elif sum > target:
                return
            else:
                for i in range(start_index,len(candidates)):
                    # if i > 0:#不可以这样写,i不一定是从0开始的
                    if i > start_index:
                        if (candidates[i] == candidates [i-1]):
                            continue
                    self.path.append(candidates[i])
                    sum += candidates[i]
                    backtracking(candidates,target,i+1,sum)
                    self.path.pop()
                    sum -= candidates[i]
            return self.result
        candidates.sort()
        print(candidates)
        return backtracking(candidates,target,0,0)

这两种结果为什么是一样的?,后面再看,目前还没有太懂

131.分割回文串 

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

切割问题和组合问题是差不多

例如对于字符串abcdef:

  • 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。
  • 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。

所以切割问题,也可以抽象为一棵树形结构,如图:

递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。

回溯三部曲

  • 递归函数参数

全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里)

本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。

  • 递归函数终止条件

从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件

那么在代码里什么是切割线呢?

在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。

  • 单层搜索的逻辑

来看看在递归循环中如何截取子串呢?

for (int i = startIndex; i < s.size(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。

首先判断这个子串是不是回文,如果是回文,就加入在path中,path用来记录切割过的回文子串。

from typing import List
import copy

class Solution:
    def __init__(self):
        self.path = []  # 收集单一结果的路径
        self.result = []  # 存储符合条件的最终结果
    
    def partition(self, s: str) -> List[List[str]]:
        # 定义一个递归的回溯函数
        def backtracking(s, start_index):
            # 终止条件:如果起始索引等于字符串长度,说明已经切割到最后
            if start_index == len(s):
                self.result.append(copy.deepcopy(self.path))  # 将当前路径的深拷贝加入结果中
                return
            # 单层搜索逻辑:从起始索引开始遍历字符串
            for i in range(start_index, len(s)):
                # 如果从start_index到i的子串是回文串
                if self.isPalindrome(s, start_index, i):
                    # 将这个回文子串加入路径
                    self.path.append(s[start_index:i + 1])  # 切片的末尾是i+1
                    # 递归调用回溯函数,处理剩余的子串
                    backtracking(s, i + 1)
                    # 回溯,移除最后一个加入路径的子串
                    self.path.pop()
            
            return self.result  # 返回结果
        
        return backtracking(s, 0)  # 从索引0开始调用回溯函数
    
    def isPalindrome(self, s, start_index, end_index):
        # 判断一个子串是否是回文串
        while start_index < end_index:  # 当起始索引小于结束索引时进行判断
            if s[start_index] != s[end_index]:  # 如果头尾字符不相等,返回False
                return False
            start_index += 1  # 移动起始索引向右
            end_index -= 1  # 移动结束索引向左
        return True  # 如果所有字符都匹配,返回True

这道题目有下几个难点:

  • 切割问题可以抽象为组合问题
  • 如何模拟那些切割线
  • 切割问题中递归如何终止
  • 在递归循环中如何截取子串
  • 如何判断回文

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/631830.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode2391. 收集垃圾的最少总时间

Every day a Leetcode 题目来源&#xff1a;2391. 收集垃圾的最少总时间 解法1&#xff1a;前缀和 收集垃圾的时间分为两部分&#xff1a; 垃圾车收拾垃圾的时间&#xff1a;垃圾车收拾一单位的任何一种垃圾都需要花费 1 分钟。三辆垃圾车行驶的时间&#xff1a;每辆垃圾车…

windows部署腾讯tmagic-editor03-DSL 解析渲染

创建项目 将上一教程中的editor-runtime和hello-editor复制过来 概念 实现 创建hello-ui目录 渲染节点 在hello-ui下创建 Component.vue 文件 由于节点的type是由业务自行定义的&#xff0c;所以需要使用动态组件渲染&#xff0c;在vue下可以使用component组件来实现 c…

软考笔记随记

原码:(0正1负) 原码是最直观的编码方式,符号位用0表示正数,用1表示负数,其余位表示数值的大小。 例如,+7的原码为00000111,-7的原码为10000111。 原码虽然直观,但直接用于加减运算会导致计算复杂,且0有两种表示(+0和-0),不唯一。 反码: 反码是在原码的基础上得…

绘唐2跟绘唐3有什么区别

绘唐2跟绘唐3有什么区别 这款产品的最大亮点在于其高度精准的语音克隆能力&#xff0c;利用先进的模型&#xff0c;能够捕捉到用户独特的音调、音高和调制方式&#xff0c;使用户能够以前所未有的方式复制和利用自己的声音。仅需10秒钟的录制时间&#xff0c;即可实现声音的克…

【C语言】自定义类型之---结构体超详解(结构体的定义使用、指针结构体,内存对齐,......代码详解)

目录 前言&#xff1a; 一&#xff1a;结构体 1.1&#xff1a;什么是结构体&#xff1f; 1.2&#xff1a;结构体类型的声明 1.3&#xff1a;结构体变量的定义 1.4&#xff1a;结构体的内存对齐 1.5&#xff1a;结构体传参 二&#xff1a;位段 2.1&#xff1a;位段是什…

docker镜像容器常用命令

常用基础命令1、docker info #查看docker版本等信息 2、docker search jenkins #搜索jenkins镜像 3、docker history nginx #查看镜像中各层内容及大小,每层对应的dockerfile中的一条指令。 4、docker network ls #显示当前主机上的所有网络 5、docker logs nginx …

2024MySQL8安装与绿色版Navicat连接【提供安装包】数据库

视频教程面向人群和使用方法&#xff1a; 1&#xff1a;大学生【解决老师作业或自己兴趣学习需要】; 2&#xff1a;第一次需要安装MySQL的开发者【需要简单使用&#xff0c;因为项目会用到】 3&#xff1a;老手二倍速&#xff0c;新手老老实实按照教程一倍速模仿视频操作&am…

【虚拟机】深入理解java虚拟机【内存溢出实例】

目录 一、问题解析 二、粉丝福利 一、问题解析 通过简单的小例子程序&#xff0c;演示java虚拟机各部分内存溢出情况&#xff1a; (1).java堆溢出&#xff1a; Java堆用于存储实例对象&#xff0c;只要不断创建对象&#xff0c;并且保证GC Roots到对象之间有引用的可达&am…

[数据集][目标检测]卡车抓斗卸车检测数据集VOC+YOLO格式213张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;213 标注数量(xml文件个数)&#xff1a;213 标注数量(txt文件个数)&#xff1a;213 标注类别…

ROS从入门到精通4-3:制作Docker镜像文件Dockerfile

目录 0 专栏介绍1 为什么需要Dockerfile&#xff1f;2 Dockerfile书写原则3 Dockerfile常用指令3.1 FROM3.2 MAINTAINER3.3 RUN3.4 ADD3.5 COPY3.6 CMD3.7 ENV3.8 EXPOSE3.9 WORKDIR3.10 ARG 4 Dockerfile构建ROS工程实例 0 专栏介绍 本专栏旨在通过对ROS的系统学习&#xff0…

数据结构与算法学习笔记一---顺序表的静态存储表示和实现(C++)

目录 前言 1.什么是顺序表 2.顺序表的静态存储表示 1.初始化 2.长度 3.数据元素 4.长度 5.获取元素下标 6.前驱节点 7.后继节点 8.插入 9.删除 10.遍历 11.测试代码 前言 这篇文章讲的是顺序表的两种实现方式。 1.什么是顺序表 线性表的顺序表示指的是用一组地址…

(论文笔记)TABDDPM:使用扩散模型对表格数据进行建模

了解diffusion model&#xff1a;什么是diffusion model? 它为什么好用&#xff1f; - 知乎 摘要 去噪扩散概率模型目前正成为许多重要数据模式生成建模的主要范式。扩散模型在计算机视觉社区中最为流行&#xff0c;最近也在其他领域引起了一些关注&#xff0c;包括语音、NLP…

LangChain搭建Agent | 使用initialize_agent

1.create_tool_calling_agent 构建agent&#xff0c;这个方法是过时了吗&#xff1f;官方文档也没更新&#xff0c;官方示例也运行错误 from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import ConfigurableField from langchain_core…

医院污水一体化处理设备有哪些

医院污水一体化处理设备通常包括以下几个主要组件&#xff1a; 预处理单元&#xff1a;用于去除污水中的固体悬浮物、颗粒物、油脂等&#xff0c;常见的预处理单元包括格栅、沉砂池、油水分离器等。生物处理单元&#xff1a;用于降解有机物质和去除氮、磷等营养物质。常见的生物…

基坑监测识别摄像机

基坑是建筑施工中的一个重要环节&#xff0c;它对整个建筑工程的安全和稳定性起着至关重要的作用。为了监测基坑的状态和确保施工的安全进行&#xff0c;基坑监测识别摄像机被广泛应用于建筑工程中。这种摄像机可以实时监测基坑施工的情况&#xff0c;识别可能存在的问题并提供…

如何在Spring启动的时候执行一些操作

如何在Spring启动的时候执行一些操作 在Spring启动的时候执行一些操作有多种方式。你可以通过实现ApplicationRunner或者CommandLineRunner接口&#xff0c;在Spring Boot应用程序启动后执行特定操作。另外&#xff0c;你也可以使用PostConstruct注解&#xff0c;在Spring Bea…

圆片/圆盘测厚设备 HW01-SG系列单点测厚仪

关键字:圆片测厚仪圆盘测厚仪, 圆形测厚仪, 单点测厚仪, 汽车工件测厚仪, 产品简介&#xff1a; 测厚仪采用上下两个对射的激光位移传感器测量圆盘状物体边缘的厚度。圆盘放置在由步进电机驱动的托盘上&#xff0c;点按测量按钮托盘旋转一周&#xff0c;可测量被测物整个圆周上…

立即注册 | 线上讲座:基于 NGINX 为现代应用构筑三大安全防线

原文作者&#xff1a;NGINX 原文链接&#xff1a;立即注册 | 线上讲座&#xff1a;基于 NGINX 为现代应用构筑三大安全防线 转载来源&#xff1a;NGINX 开源社区 NGINX 唯一中文官方社区 &#xff0c;尽在 nginx.org.cn 基本信息 课程主题&#xff1a;基于 NGINX 为现代应用构…

大模型算法(零) - Transformer中的细节与实现

讲transformer的文章已经铺天盖地了&#xff0c;但是大部分都是从原理的角度出发的文章&#xff0c;原理与实现之间的这部分讲解的较少&#xff0c;想要了解实现细节&#xff0c;还是要去看代码才行。记录一下自己学习过程中遇见的细节问题和实现问题。 Transformer整体架构 图…

Android面试题之Kotlin的几种常见的类

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 初始化的顺序 主构造函数里声明的属性 类级别的属性赋值 init初始化块里的属性赋值和函数调用 次构造函数里的属性赋值和函数调用 延迟初始…