算法训练Day28 | LeetCode93.复原IP地址(回溯算法中的切割问题2);78 子集(每个节点都收集结果);90.子集II(子集问题+去重)

news2025/8/7 6:06:54

前言:算法训练系列是做《代码随想录》一刷,个人的学习笔记和详细的解题思路,总共会有60篇博客来记录,记录结构上分为 思路,代码实现,复杂度分析,思考和收获,四个方面。如果这个系列的博客可以帮助到读者,就是我最大的开心啦,一起LeetCode一起进步呀;)

目录

LeetCode93.复原IP地址

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

Leetcode. 78 子集

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

Leetcode90.子集II

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


LeetCode93.复原IP地址

链接:93. 复原 IP 地址 - 力扣(LeetCode)

1. 思路

做这道题目之前,最好先把**131.分割回文串 (opens new window)**这个做了。

这道题目相信大家刚看的时候,应该会一脸茫然。其实只要意识到这是切割问题,切割问题就可以使用回溯搜索法把所有可能性搜出来,然后根据题意进行一些合法性的判断,和刚做过的**131.分割回文串 (opens new window)**就十分类似了。切割问题可以抽象为树型结构,如图:

2. 代码实现

2.1 递归参数

在**131.分割回文串 (opens new window)**中我们就提到切割问题类似组合问题。

startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置;本题我们还需要一个变量pointNum,记录添加逗点的数量。

所以代码如下:

# python3
class Solution:
    def __init__(self):
        self.result = []

    def restoreIpAddresses(self, s: str) -> List[str]:
        '''
    本质切割问题使用回溯搜索法,本题只能切割三次,所以纵向递归总共四层
    因为不能重复分割,所以需要start_index来记录下一层递归分割的起始位置
    添加变量point_num来记录逗号的数量[0,3]
        '''
        self.result.clear()
        if len(s) > 12: return []
        self.backtracking(s, 0, 0)
        return self.result

    def backtracking(self, s: str, start_index: int, point_num: int) -> None:

2.2 递归终止条件

终止条件和**131.分割回文串 (opens new window)**情况就不同了,本题明确要求只会分成4段,所以不能用切割线切到最后作为终止条件,而是分割的段数作为终止条件;

pointNum表示逗点数量,pointNum为3说明字符串分成了4段了,然后验证一下第四段是否合法,如果合法就加入到结果集里;

# python3
def backtracking(self, s: str, start_index: int, point_num: int) -> None:
    # Base Case
    if point_num == 3:
        if self.is_valid(s, start_index, len(s)-1):
            self.result.append(s[:])
        return

2.3 单层搜索的逻辑

在**131.分割回文串 (opens new window)**中已经讲过在循环遍历中如何截取子串;

for (int i = startIndex; i < s.size(); i++)循环中 [startIndex, i] 这个区间就是截取的子串,需要判断这个子串是否合法。如果合法就在字符串后面加上符号.表示已经分割,如果不合法就结束本层循环,如图中剪掉的分支:

然后就是递归和回溯的过程:

递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.),同时记录分割符的数量pointNum 要 +1。

回溯的时候,就将刚刚加入的分隔符. 删掉就可以了,pointNum也要-1。

# python3
# 单层递归逻辑
for i in range(start_index, len(s)):
    # [start_index, i]就是被截取的子串
    if self.is_valid(s, start_index, i):
        s = s[:i+1] + '.' + s[i+1:]
        self.backtracking(s, i+2, point_num+1)  
				# 在填入.后,下一子串起始后移2位
        s = s[:i+1] + s[i+2:]    # 回溯
    else:
        # 若当前被截取的子串大于255或者大于三位数,直接结束本层循环
        break

2.4 判断子串是否合法

最后就是在写一个判断段位是否是有效段位了。

主要考虑到如下三点:

  • 段位以0为开头的数字不合法
  • 段位里有非正整数字符不合法
  • 段位如果大于255了不合法

代码如下:

def is_valid(self, s: str, start: int, end: int) -> bool:
    if start > end: return False
    # 若数字是0开头,不合法
    if s[start] == '0' and start != end:
        return False
    if not 0 <= int(s[start:end+1]) <= 255:
        return False
    return True

2.5 总体代码如下

class Solution:
    def __init__(self):
        self.result = []

    def restoreIpAddresses(self, s: str) -> List[str]:
        '''
    本质切割问题使用回溯搜索法,本题只能切割三次,所以纵向递归总共四层
    因为不能重复分割,所以需要start_index来记录下一层递归分割的起始位置
    添加变量point_num来记录逗号的数量[0,3]
        '''
        self.result.clear()
        if len(s) > 12: return []
        self.backtracking(s, 0, 0)
        return self.result

    def backtracking(self, s: str, start_index: int, point_num: int) -> None:
        # Base Case
        if point_num == 3:
            if self.is_valid(s, start_index, len(s)-1):
                self.result.append(s[:])
            return
        # 单层递归逻辑
        for i in range(start_index, len(s)):
            # [start_index, i]就是被截取的子串
            if self.is_valid(s, start_index, i):
                s = s[:i+1] + '.' + s[i+1:]
                self.backtracking(s, i+2, point_num+1)  # 在填入.后,下一子串起始后移2位
                s = s[:i+1] + s[i+2:]    # 回溯
            else:
                # 若当前被截取的子串大于255或者大于三位数,直接结束本层循环
                break

    def is_valid(self, s: str, start: int, end: int) -> bool:
        if start > end: return False
        # 若数字是0开头,不合法
        if s[start] == '0' and start != end:
            return False
        if not 0 <= int(s[start:end+1]) <= 255:
            return False
        return True

3. 复杂度分析

(我自己这样子分析的,正确性有待考究)

  • 时间复杂度:O(2^N)

    因为每一个元素的状态无外乎割与不割,所以时间复杂度为O(2^n)

  • 空间复杂度:O(1)

    本题递归深度最多为4,递归栈所用空间为常数级别,result和path都是全局变量,传的都是引用,不会有额外申请新的空间;

4. 思考与收获

  1. 在**131.分割回文串 (opens new window)中我列举的分割字符串的难点,本题都覆盖了。而且本题还需要操作字符串添加逗号作为分隔符,并验证区间的合法性。可以说是131.分割回文串 (opens new window)**的加强版;
  2. 关键是按照流程依次解决各个难点问题,思路清晰。

Reference: 代码随想录 (programmercarl.com)

本题学习时间:90分钟。


Leetcode. 78 子集

 链接:78. 子集 - 力扣(LeetCode)

1. 思路

求子集问题和**77.组合 (opens new window)131.分割回文串 (opens new window)**又不一样了。

子集问题 vs 组合问题分割问题

如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!

其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!

什么时候for可以从0开始呢?

求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题后续就会讲到的。 

以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:

从图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。

2. 代码实现

2.1 递归函数参数

全局变量数组path为子集收集元素,二维数组result存放子集组合。(也可以放到递归函数参数里);递归函数参数在上面讲到了,需要startIndex。

class Solution:
    def __init__(self):
        self.path: List[int] = []
        self.paths: List[List[int]] = []

    def subsets(self, nums: List[int]) -> List[List[int]]:
        self.paths.clear()
        self.path.clear()
        self.backtracking(nums, 0)
        return self.paths

    def backtracking(self, nums: List[int], start_index: int) -> None:

2.2 递归终止条件

从图中可以看出:

剩余集合为空的时候,就是叶子节点。

那么什么时候剩余集合为空呢?就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下:

def backtracking(self, nums: List[int], start_index: int) -> None:
    # Base Case
    if start_index == len(nums):
        return

 💡 其实可以不需要加终止条件,因为下面for循环的条件,startIndex >= nums.size(),本层for循环本来也结束了

2.3 单层搜索逻辑

求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树

那么单层递归逻辑代码如下:

# 单层递归逻辑
for i in range(start_index, len(nums)):
    self.path.append(nums[i])
    self.backtracking(nums, i+1)
    self.path.pop()     # 回溯

2.4 整体代码

# 回溯算法 子集问题
# time:O(2^N);space:O(N)
class Solution(object):
    # 定义好全局变量
    def __init__(self):
        self.path = []
        self.result = []

    def subsets(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if nums == []: return [[]]
        self.backtracking(nums,0)
        return self.result

    def backtracking(self,nums,startIndex):
        # 收集结果要在终止条件前面,不然会错过叶子节点的值
        self.result.append(self.path[:])
        # 终止条件
        if startIndex > len(nums)-1:
            return 
        # 单层循环逻辑
        for i in range(startIndex,len(nums)):
            self.path.append(nums[i])
            self.backtracking(nums,i+1)
            self.path.pop()

3. 复杂度分析

  • 时间复杂度:O(2^n)

    因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)

  • 空间复杂度:O(n)

    递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

4. 思考与收获

  1. 子集问题整体来说并不难,但是要清楚子集问题和组合问题、分割问题的的区别,子集是收集树形结构中树的所有节点的结果,而组合问题、分割问题是收集树形结构中叶子节点的结果;
  2. startIndex的作用和组合问题以及切割问题中都是一样的,保证剩余元素进入下一层递归的时候,都是从当前元素的后面一个开始取的,否则会有1,2和2,1的重复出现;
  3. 其实终止条件是可以不写的,因为for循环中starIndex≥ len(nums)的时候根本也不会进入for loop ,可能担心不写终止条件会不会无限递归?并不会,因为每次递归的下一层就是从i+1开始的;
  4. 本题的集合问题也是没有任何剪枝操作的,因为我们本来就要遍历整棵树,然后收集树上每个节点的集合;
  5. 收集子集的代码必须放在终止条件的上面,不然会错过叶子节点的子集情况。

Reference: 代码随想录 (programmercarl.com)

本题学习时间:60分钟。


Leetcode90.子集II

 链接:90. 子集 II - 力扣(LeetCode)

1. 思路

这道题目和**78.子集 (opens new window)**区别就是集合里有重复元素了,而且求取的子集要去重。

那么关于回溯算法中的去重问题,在40.组合总和II (opens new window)中已经详细讲解过了,和本题是一个套路剧透一下,后期要讲解的排列问题里去重也是这个套路,所以理解“树层去重”和“树枝去重”非常重要

用示例中的[1, 2, 2] 来举例,如图所示: (注意去重需要先对集合排序

从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集!

2. 代码实现

# 回溯算法 子集问题
# time:O(2^N);space:O(N)
class Solution(object):
    # 定义全局变量
    def __init__(self):
        self.path = []
        self.result = []

    def subsetsWithDup(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        # 排序 time:O(NlogN)
        nums.sort()
        if nums == []: return [[]]
        self.backtracking(nums,0,[0]*len(nums))
        return self.result

    def backtracking(self,nums,startIndex,used):
        # 收集结果,每个节点都收集
        self.result.append(self.path[:])
        # 递归终止条件
        if startIndex > len(nums)-1:
            return 
        # 单层递归逻辑
        for i in range(startIndex,len(nums)):
            # 去重逻辑
            if i>0 and nums[i] == nums[i-1] and used[i-1] == 0:
                continue 
            self.path.append(nums[i])
            used[i] = 1
            self.backtracking(nums,i+1,used)
            self.path.pop()
            used[i] = 0

3. 复杂度分析

  • 时间复杂度:O(2^n)

    因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)

  • 空间复杂度:O(n)

    递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

4. 思考与收获

  1. 本题就是组合总和II + 子集,把里面的知识点综合运用一下,没有新的知识点;
  2. 本题其实也可以不通过used数组去重,但是这种方法更好理解更通用,之后的排列问题中还要用到的,所以掌握这一种就可以;
  3. 本题的终止条件实际上可以省略,因为startIndex≥ len(nums) 之后已经不可以进入for loop了;

Reference: 代码随想录 (programmercarl.com)

本题学习时间:60分钟。


本篇学习时间为3个多小时,总结字数为6000+;针对回溯算法中的切割问题和子集问题做了相应的练习,切割问题本质上也是一种组合问题,子集问题与前两者的区别是,子集问题收集所有节点上的数值。(求推荐!)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过DIN算法进行深度特征组合商品推荐 数据+代码(可作为毕设)

案例知识点 推荐系统任务描述:通过用户的历史行为(比如浏览记录、购买记录等等)准确的预测出用户未来的行为;好的推荐系统不仅如此,而且能够拓展用户的视野,帮助他们发现可能感兴趣的却不容易发现的item;同时将埋没在长尾中的好商品推荐给可能感兴趣的用户。 方法概述:…

美团SemEval 2022结构化情感分析跨语言赛道冠军方法总结

总第547篇2022年 第064篇美团语音交互部针对跨语言结构化情感分析任务中缺少小语种的标注数据、传统方法优化成本高昂的问题&#xff0c;通过利用跨语言预训练语言模型、多任务和数据增强方法在不同语言间实现低成本的迁移&#xff0c;相关方法获得了SemEval 2022结构化情感分析…

使用dispatchEvent解决重叠元素响应事件问题

.npm 下的缓存文件太多怎么办&#xff1f;.npm 下缓存的包长时间未清理&#xff0c;占用空间太大怎么办&#xff1f; 查看磁盘占用情况 linux 系统里&#xff0c;查看磁盘占用情况&#xff1a;df -h 1.查看单个目录磁盘占用情况du -sh /指定目录 2.查看所有目录的磁盘占用情况…

基于VGG与LSTM实现针对图片的问答任务 数据+代码 可以作为毕设

任务描述:本教程将通过深度学习的方式实现一个简单的视觉问答模型,视觉问答的任务内容是将一张图片和一个自然语言问题作为输入,结合这两种信息,机器生成一条自然语言答案。本教程通过数据准备,视觉问答模型构建,视觉问答模型训练,视觉问答模型评估,视觉问答模型预测等…

2022年亚太C题资料汇总更新目录

1.17版本更新内容&#xff1a; 为大家收集了一套网上的成品论文&#xff0c;切记只能借鉴&#xff0c;不可全抄 1.16版本更新内容&#xff1a; 根据半成品论文中提及的加分点&#xff0c;为大家收集了本次比赛中作图较为好看的方式。 1.15版本更新内容&#xff1a; 对上传…

Hbuilder出现 CR LF

今天打开Hbuilder编辑器发现&#xff0c;咦&#xff0c;怎么变成这个样子了&#xff0c;我设置了啥&#xff1f; 最终尝试寻找了半天&#xff0c;原来这是显示了换行符号 &#xff0c;取消这个勾选就OK

西门子S7-200 SMART(6ES7 288-1ST40-0AA0)相关与晨控智能CK-FR08-E00关于modbus tcp 通信配置指南

西门子S7-200 SMART(6ES7 288-1ST40-0AA0)相关与晨控智能CK-FR08-E00关于modbus tcp 通信配置指南 准备阶段 软件&#xff1a;STEP 7-MicroWIN SMART PLC:S7-200 SMATR (6ES7 288-1ST40-OAAO) 读卡器&#xff1a;CK-FR08-E00 交换机&#xff1a;标准POE交换机 电源&#x…

华为云开发者官网首页焕新升级,赋能开发者云上成长

摘要&#xff1a;近日&#xff0c;华为云开发者官网首页迎来全新改版升级。本文分享自华为云社区《华为云开发者官网首页焕新升级&#xff0c;赋能开发者云上成长》&#xff0c;作者&#xff1a; 华为云社区精选 。 近日&#xff0c;华为云开发者官网首页迎来全新改版升级&…

3.81 OrCAD软件绘制原理图时如何使用任意角度的走线?OrCAD软件怎么统一查看哪些元器件是没有PCB封装的?

笔者电子信息专业硕士毕业&#xff0c;获得过多次电子设计大赛、大学生智能车、数学建模国奖&#xff0c;现就职于南京某半导体芯片公司&#xff0c;从事硬件研发&#xff0c;电路设计研究。对于学电子的小伙伴&#xff0c;深知入门的不易&#xff0c;特开次博客交流分享经验&a…

UNI-APP_开发支付宝小程序注意事项与解决方法,支付宝小程序图片显示问题

一、编译后&#xff0c;微信小程序上图片图标显示正常&#xff0c;但是一到支付宝小程序图片图标显示就不正常如下图&#xff1a; 微信显示 支付宝显示 官方文档&#xff1a;https://opendocs.alipay.com/mini/component/image //修改前---会出问题 <image src"&qu…

WSL2编译ijkplayer

Windows 11 专业版 22H2 22621.819 应用商店安装 Ubuntu 22.04.1 LTS 控制面板——程序和功能——启用或关闭Windows功能——适用于 Linux的Windows子系统 Error: 0x800701bc WSL 2 ??? 升级WSL https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.ms…

香港阿里云服务器被攻击了怎么办

香港阿里云被流量攻击了&#xff0c;一说到防御攻击&#xff0c;很多用户可能会想到CDN、高防IP等防御产品&#xff0c;这个思路是不错的。但是香港阿里云相对于国内的阿里云而言&#xff0c;更多情况下是无法直接使用国内的CDN和高防IP来防御的。大部分用户在使用香港阿里云时…

Go运行时的内存分配器以及消耗指定大小的内存(C语言)

对于go语言在运行时的一些内存分配&#xff0c;想要详细的了解&#xff0c;我们会用到自带的runtime.MemStats&#xff0c;有很多具体的细节实现&#xff0c;而不是简单的只看任务管理器中的内存分配。 我们先来看下这个记录内存分配器的结构体 type MemStats struct {Alloc …

【新知实验室】腾讯云TRTC初体验

一、前言 今年腾讯云音视频发布了“三合一”的RT-ONE™网络。该网络整合了腾讯云实时通信网络&#xff08;TRTC&#xff09;、即时通信网络&#xff08;IM&#xff09;以及流媒体分发网络&#xff08;CDN&#xff09;三张网络&#xff0c;为业界最完整的音视频通信PaaS平台构建…

【App自动化测试】(八)三种等待方式——强制等待、隐式等待、显示等待

目录1. 为什么要添加等待&#xff1f;2. 三种等待方式3. 强制&#xff08;直接&#xff09;等待4. 隐式等待4.1 隐式等待说明4.2 隐式等待无法解决的问题5. 显式等待5.1 为什么要使用显示等待机制&#xff1f;5.1.1 Html文件加载顺序5.1.2 为什么要使用显示等待机制&#xff1f…

简化工作和生活的 7 个在线地图制作平台分享

地图制作是数据和艺术的结合&#xff0c;数据可以传达人们想要的信息&#xff0c;而艺术是传达信息的一种方式&#xff0c;两者的正确组合创造了完美的地图。 每个平台在功能和价格方面都是独一无二的&#xff0c;有人可能认为创建自定义地图需要高级制图知识或复杂的地理信息…

从react源码看hooks的原理

React暴露出来的部分Hooks //packages/react/src/React.js export {...useCallback,useContext,useEffect,useLayoutEffect,useMemo,useReducer,useRef,useState,... }功能描述 useState、useReducer: 状态值相关useEffect、useLayoutEffect: 生命周期相关useContext: 状态共…

Java多线程(一)——多线程的创建

多线程 在计算机中为了提高内存和资源的利用率&#xff0c;引入了并发编程的思想&#xff1b;多进程和多线程都能实现并发编程&#xff0c;但是多线程相对于多进程更“轻量”&#xff0c;&#xff08;多线程和多线程的关系和区别&#xff09;&#xff0c;所以这篇博客将着重讲…

CPU

文章目录CPU是什么CPU的构成1) 控制器2) 运算器3) 存储单元&#xff08;寄存器和高速缓存&#xff09;常见的CPU类型1) 单核 CPU2) 双核 CPU3) 四核 CPUCPU是什么 CPU 全称 Central Processing Unit&#xff0c;中文可译为中央处理器或者微处理器&#xff0c;或直接简称为处理…

挂脖式运动蓝牙耳机推荐,目前适合运动佩戴的五款耳机推荐

在科技的不断进步下&#xff0c;新型的骨传导耳机也是逐渐成为我们生活日常中的主流&#xff0c;其特殊的发声原理成为了我们喜爱的重点之一&#xff0c;也有些伙伴们还在边缘徘徊&#xff0c;想要入手骨传导耳机但又怕踩坑得不到好的体验&#xff0c;刚好小编在使用骨传导耳机…