算法训练Day36 贪心算法系列 - 重叠区间问题 | LeetCode435. 无重叠区间;763. 划字母区间;56.合并区间

news2024/11/23 16:54:01

前言:

算法训练系列是做《代码随想录》一刷,个人的学习笔记和详细的解题思路,总共会有60篇博客来记录,计划用60天的时间刷完。 

内容包括了面试常见的10类题目,分别是:数组,链表,哈希表,字符串,栈与队列,二叉树,回溯算法,贪心算法,动态规划,单调栈。

博客记录结构上分为 思路,代码实现,复杂度分析,思考和收获,四个方面。

如果这个系列的博客可以帮助到读者,就是我最大的开心啦,一起LeetCode一起进步呀;)
 

目录

 LeetCode435. 无重叠区间

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode763. 划分字母区间

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

Leetcode 56. 合并区间

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


 LeetCode435. 无重叠区间

 链接:435. 无重叠区间 - 力扣(LeetCode)

1. 思路

**相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?**这其实是一个难点!

排序和遍历顺序?

  • 按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的;
  • 按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历;
  • 如果按照左边界排序,还从左向右遍历的话,其实也可以,逻辑会有所不同;

题意就是求非交叉区间的最大个数!

一些同学做这道题目可能真的去模拟去重复区间的行为,这是比较麻烦的,还要去删除区间。题目只是要求移除区间的个数,没有必要去真实的模拟删除区间!

按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。此时问题就是要求非交叉区间的最大个数。

贪心算法的思想

右边界排序之后:

  • 局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉;
  • 全局最优:选取最多的非交叉区间。
  • 局部最优推出全局最优,试试贪心!

这里记录非交叉区间的个数还是有技巧的,如图:

区间,1,2,3,4,5,6都按照右边界排好序。

每次取非交叉区间的时候,都是可右边界最小的来做分割点(这样留给下一个区间的空间就越大),所以第一条分割线就是区间1结束的位置;

接下来就是找大于区间1结束位置的区间,是从区间4开始。那有同学问了为什么不从区间5开始?别忘已经是按照右边界排序的了;

区间4结束之后,在找到区间6,所以一共记录非交叉区间的个数是三个;总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。

2. 代码实现

# 贪心算法
# time:O(NlogN);space:O(n)
class Solution(object):
    def eraseOverlapIntervals(self, intervals):
        """
        :type intervals: List[List[int]]
        :rtype: int
        """
        # 细节:当x[1]相同的时候,随便排序都可以,不影响结果
        intervals.sort(key = lambda x: x[1])
        # 记录非交叉区间的个数
        count = 1
        # 记录区间分割点
        rightLine = intervals[0][1]
        for i in range(1,len(intervals)):
            if intervals[i][0] >= rightLine:
                count += 1
                rightLine = intervals[i][1]
        return len(intervals)-count

3. 复杂度分析

  • 时间复杂度:O(nlog n) ,有一个快排;
  • 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间;

4. 思考与收获

  1. 本题难度级别可以算是hard级别的!

    总结如下难点:

    • 难点一:一看题就有感觉需要排序,但究竟怎么排序,按左边界排还是右边界排。
    • 难点二:排完序之后如何遍历,如果没有分析好遍历顺序,那么排序就没有意义了。
    • 难点三:直接求重复的区间是复杂的,转而求最大非重复区间个数。
    • 难点四:求最大非重复区间个数时,需要一个分割点来做标记。

    这四个难点都不好想,但任何一个没想到位,这道题就解不了;

  2. 贪心就是这样,代码有时候很简单(不是指代码短,而是逻辑简单),但想法是真的难!这和动态规划还不一样,动规的代码有个递推公式,可能就看不懂了,而贪心往往是直白的代码,但想法读不懂,哈哈;

  3. 本题其实和**452.用最少数量的箭引爆气球 (opens new window)** 非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。

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

本题学习时间:40分钟。


LeetCode763. 划分字母区间

 链接:763. 划分字母区间 - 力扣(LeetCode)

1. 思路

一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索;题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢?

如果没有接触过这种题目的话,还挺有难度的。

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。

可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

2. 代码实现

# 贪心算法
# time:O(N);space:O(1)
class Solution(object):
    def partitionLabels(self, s):
        """
        :type s: str
        :rtype: List[int]
        """
        # i为字符,hash[i]为字符出现的最后位置
        record = [0]*26
        result = []
        left = 0
        right = 0
        # 统计每一个字符最后出现的位置
        for i in range(len(s)):
            record[ord(s[i])-ord("a")] = i
        for i in range(len(s)):
            # 找到字符出现的最远边界
            right = max(right,record[ord(s[i])-ord("a")])
            if i == right:
                result.append(right-left+1)
                left = i+1
        return result

3. 复杂度分析

  • 时间复杂度:O(N)

    其中N为字符串长度的大小,需要遍历字符串两遍;

  • 空间复杂度:O(1)

    使用的hash数组的大小是固定的;

4. 思考与收获

  1. 这道题目leetcode标记为贪心算法,说实话,我没有感受到贪心,找不出局部最优推出全局最优的过程。就是用最远出现距离模拟了圈字符的行为,但这道题目的思路是很巧妙的!

  2. (二刷再看)这里提供一种与**452.用最少数量的箭引爆气球 (opens new window)435.无重叠区间 (opens new window)相同的思路。统计字符串中所有字符的起始和结束位置,记录这些区间(实际上也就是435.无重叠区间 (opens new window)**题目里的输入),将区间按左边界从小到大排序,找到边界将区间划分成组,互不重叠。找到的边界就是答案。

    class Solution {
    public:
        static bool cmp(vector<int> &a, vector<int> &b) {
            return a[0] < b[0];
        }
        // 记录每个字母出现的区间
        vector<vector<int>> countLabels(string s) {
            vector<vector<int>> hash(26, vector<int>(2, INT_MIN));
            vector<vector<int>> hash_filter;
            for (int i = 0; i < s.size(); ++i) {
                if (hash[s[i] - 'a'][0] == INT_MIN) {
                    hash[s[i] - 'a'][0] = i;
                }
                hash[s[i] - 'a'][1] = i;
            }
            // 去除字符串中未出现的字母所占用区间
            for (int i = 0; i < hash.size(); ++i) {
                if (hash[i][0] != INT_MIN) {
                    hash_filter.push_back(hash[i]);
                }
            }
            return hash_filter;
        }
        vector<int> partitionLabels(string s) {
            vector<int> res;
            // 这一步得到的 hash 即为无重叠区间题意中的输入样例格式:区间列表
            // 只不过现在我们要求的是区间分割点
            vector<vector<int>> hash = countLabels(s);
            // 按照左边界从小到大排序
            sort(hash.begin(), hash.end(), cmp);
            // 记录最大右边界
            int rightBoard = hash[0][1];
            int leftBoard = 0;
            for (int i = 1; i < hash.size(); ++i) {
                // 由于字符串一定能分割,因此,
                // 一旦下一区间左边界大于当前右边界,即可认为出现分割点
                if (hash[i][0] > rightBoard) {
                    res.push_back(rightBoard - leftBoard + 1);
                    leftBoard = hash[i][0];
                }
                rightBoard = max(rightBoard, hash[i][1]);
            }
            // 最右端
            res.push_back(rightBoard - leftBoard + 1);
            return res;
        }
    };
    

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

本题学习时间:40分钟。


Leetcode 56. 合并区间

链接:56. 合并区间 - 力扣(LeetCode) 

1. 思路

大家应该都感觉到了,此题一定要排序,那么按照左边界排序,还是右边界排序呢?都可以!

那么我按照左边界排序:

  • 排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了;
  • 整体最优:合并所有重叠的区间。
  • 局部最优可以推出全局最优,找不出反例,试试贪心。

如何判断重复?

那有同学问了,本来不就应该合并最大右边界么,这和贪心有啥关系?有时候贪心就是常识!哈哈

按照左边界从小到大排序之后,如果 intervals[i][0] < intervals[i - 1][1] 即intervals[i]左边界 < intervals[i - 1]右边界,则一定有重复,因为intervals[i]的左边界一定是大于等于intervals[i - 1]的左边界。

即:intervals[i]的左边界在intervals[i - 1]左边界和右边界的范围内,那么一定有重复!

这么说有点抽象,看图:(注意图中区间都是按照左边界排序之后了

如何模拟合并区间呢?

其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。

2. 代码实现

# 贪心算法
# time:O(NlogN);space:O(N)
class Solution(object):
    def merge(self, intervals):
        """
        :type intervals: List[List[int]]
        :rtype: List[List[int]]
        """
        # 按照区间左边界从小到大排序
        intervals.sort(key=lambda x: x[0])
        # 先初始化result的第一个元素为第一个intervals里面的区间
        result= [intervals[0]]
        # 从第二个区间开始遍历intervals
        for i in range(1,len(intervals)):
            # 如果当前interval的左边界小于或者等于
            # result最后一个元素的右边界,说明有重叠区间
            if intervals[i][0] <= result[-1][1]:
                # 合并区间,左边界因为排序了,所以不变
                # 更新右边界,为以前值和现在的值的最大值
                result[-1][1] = max(result[-1][1],intervals[i][1])
            else:
                # 如果最后一个区间没有合并,将其加入result
                result.append(intervals[i])
        return result

3. 复杂度分析

  • 时间复杂度:O(nlog n)

    其中N为intervals数组的长度,有一个快排;

  • 空间复杂度:O(n)

    有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间;

4. 思考与收获

  1. 对于贪心算法,很多同学都是:如果能凭常识直接做出来,就会感觉不到自己用了贪心, 一旦第一直觉想不出来, 可能就一直想不出来了。跟着「代码随想录」刷题的录友应该感受过,贪心难起来,真的难。那应该怎么办呢?

    正如我贪心系列开篇词**关于贪心算法,你该了解这些! (opens new window)**中讲解的一样,贪心本来就没有套路,也没有框架,所以各种常规解法需要多接触多练习,自然而然才会想到。「代码随想录」会把贪心常见的经典题目覆盖到,大家只要认真学习打卡就可以了。

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

本题学习时间:40分钟。


本篇学习时间约为2小时,总结字数为5000+;本篇是贪心算法的重叠区间专题,再加上Day35的最后一道题:用最少数量的箭引爆气球,这四道题是重叠区间的经典题目,都属于那种看起来好复杂,但一看贪心解法,惊呼:这么巧妙! 做过了也就会了,没做过就很难想出来。(求推荐!)

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

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

相关文章

【Linux】快捷键

Ctrl C&#xff1a;终止当前命令

星环科技数据中台解决方案,助力某政府机构建设新型智慧城市

客户背景 城市&#xff0c;是人们工作生活的栖息地&#xff0c;也是展示发展成果的全景图。某政府机构不仅注重城市“中枢大脑”的建设&#xff0c;而且兼顾“神经末梢”的需求&#xff0c;既有技术进步的“面子”&#xff0c;更有民生保障的“里子”。站在新的起点上&#xff…

Linux计划任务管理

一&#xff0c;计划任务管理&#xff1a; 任务管理很宽泛&#xff0c;这里是指的计划任务管理&#xff0c;在指定的时间执行。 1&#xff0c;at命令 &#xff1a; 由atd守护进程来执行&#xff0c;atd进程会定期检查系统上的 /var/spool/at 目录&#xff0c;获取at命令写入的任…

如何建立你的财务体系?

天下人都想同时实现财务管理民主自由&#xff0c;换言之一下&#xff0c;你躺在毛里求斯的海滩上&#xff0c;吹着南风&#xff0c;晒着月亮&#xff0c;还有总收入源源不绝的流向&#xff0c;阿涅尔&#xff1f; 那么&#xff0c;怎样同时实现&#xff1f;坚信我们都知道投资…

准备大半年,面试频繁受挫,Java岗面试为何越来越难?

作为一名优秀的程序员&#xff0c;技术面试都是不可避免的一个环节&#xff0c;一般技术面试官都会通过自己的方式去考察程序员的技术功底与基础理论知识。 如果你参加过一些大厂面试&#xff0c;肯定会遇到一些这样的问题&#xff1a; 1、看你项目都用的框架&#xff0c;熟悉…

什么是链动2+1模式?链动2+1模式玩法解析

链动21模式玩法解析 模式框架&#xff1a;代理、老板 奖励机制&#xff1a;平级奖、见点奖、平级奖、帮扶基金、分红奖 商业模式玩法&#xff1a;每一个代理晋升为老板的同时&#xff0c;都需要给上级代理留下“两个原始种子用户”&#xff0c;咱们这里就俗称“感恩机制”。…

双十二有哪些数码好物值得入手、双十二必买数码好物清单

双十二马上就到了&#xff0c;相信很多小伙伴已经按耐不住想要入手了吧&#xff1f;但如果目前还没什么头绪&#xff0c;不知道买什么的话&#xff0c;现在就不妨来抄一下作业吧&#xff01;近期我整理了一份双十二数码好物清单&#xff0c;都是我从用户评价、产品亮点、折扣力…

python中base64编码

1. base64编码简介 用记事本打开exe、jpg、pdf这些文件时&#xff0c;我们都会看到一大堆乱码&#xff0c;因为二进制文件包含很多无法显示和打印的字符&#xff0c;所以&#xff0c;如果要让记事本这样的文本处理软件能处理二进制数据&#xff0c;就需要一个二进制到字符串的…

Perl与JS的对比分析(数组、哈希)

一、数组 可以对数组进行增删&#xff0c;插入。与JS不同的是这些函数都是全局的&#xff0c;JS则是挂在Array.prototype上。 1&#xff0c;对数组尾部的操作pop&#xff08;删除最后的元素&#xff09;、push&#xff08;在尾部添加&#xff09; 1 2 3 goods qw/pen penci…

[附源码]JAVA毕业设计婚纱影楼服务管理(系统+LW)

[附源码]JAVA毕业设计婚纱影楼服务管理&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术…

BluePrism里WorkQueue的几种传法和区别

WorkQueue是开发中的好帮手&#xff0c;流程间任务流转非常方便&#xff0c;基本可以取代数据库的场景。 一.循环SourceData单行传入 把Queue的Item Key搭好New Item Data本来为空的collection&#xff0c;每次循环增加一行把Item Key加入New Item Data.Item KeyNew Item Data加…

SpringBoot+Vue实现前后端分离的员工日志管理信息系统

文末获取源码 开发语言&#xff1a;Java 使用框架&#xff1a;spring boot 前端技术&#xff1a;JavaScript、Vue.js 、css3 开发工具&#xff1a;IDEA/MyEclipse/Eclipse、Visual Studio Code 数据库&#xff1a;MySQL 5.7/8.0 数据库管理工具&#xff1a;phpstudy/Navicat JD…

以工程化路径破题,中国系统推动数据要素化

光阴似箭&#xff0c;大数据产业已走过十余载。 如今&#xff0c;人们在各种数字化场景中深刻感受到数据所蕴含的巨大价值&#xff0c;也深深为数据滥用、数据窃取、数据非法交易等各种乱象而担忧。似乎&#xff0c;数据安全与和数据流动、共享与使用已成一对天然的“矛盾”&a…

15.前端笔记-CSS-PS切图

1、图片格式 格式优点jpg同jpeg,产品类图片常用&#xff0c;高清&#xff0c;颜色多gif可以保存成透明背景和动画效果&#xff0c;图片小动画常用&#xff0c;最多存储256色png结合了jpg和gif的优点&#xff0c;保存为背景透明&#xff0c;存储形式丰富psdphotoshop的文件格式…

JMM(Java Memory Model)

Java虚拟机规范中定义了Java内存模型&#xff08;共享内存模型&#xff0c;实现线程与线程之间的通信&#xff0c;其中主内存是逻辑空间&#xff09;&#xff0c;用于屏蔽掉各种硬件和操作系统的内存访问差异&#xff0c;以实现让Java程序在各种平台下都能达到一致的并发效果&a…

30张图 讲清楚Redis Cluster

今天下午和一位同学聊Redis集群&#xff0c;这玩意真没那么简单&#xff0c;内容非常多。 Redis Cluster是Redis官方提供的Redis集群功能。 1.为什么要实现Redis Cluster 1.主从复制不能实现高可用 2.随着公司发展&#xff0c;用户数量增多&#xff0c;并发越来越多&#x…

基于微分段的东西向安全防护,如何提升数据中心运维效率?|社区成长营分享回顾

在社区成长营网络与安全第二期分享中&#xff0c;SmartX 高级产品营销经理张涛介绍了原生于超融合系统的微分段防火墙和其工作模式的特点&#xff08;点击查看第二期图文总结&#xff09;。 网络与安全第三期分享继续由张涛老师提供&#xff0c;以下为内容回顾。 SmartX 社区成…

品质创未来!流辰信息技术公司实力谱新章!

对于一个企业而言&#xff0c;一辈子用心做好一件事就是对社会最大的贡献。自从深耕于低代码开发平台领域以来&#xff0c;流辰信息技术公司便时刻保持着奋斗和创新的研发心态&#xff0c;不仅增强自主创新能力&#xff0c;而且还积极扩大队伍团队和实力&#xff0c;立志为每一…

【新知实验室-TRTC开发】实时音视频之集美真心话

目录 前言&#xff1a; 一、说说TRTC呗 语音互动直播 语聊房 语音电台 二、成为TRTC的体验官 1、1分钟了解TRTC产品 2、2分钟新手入门 3、别忘了入场券&#xff0c;也别告诉别人哦 三、3分钟完成新应用搭建 1、解压下载源码 2、修改JS文件 3、修改index文件 4、和集…

【Android App】蓝牙的设备配对、音频传输、点对点通信的讲解及实战(附源码和演示 超详细)

需要源码请点赞关注收藏后评论区留言私信~~~ 一、蓝牙设备配对 Android提供了蓝牙模块的管理工具&#xff0c;名叫BluetoothAdapter。下面是BluetoothAdapter类常用的方法说明&#xff1a; getDefaultAdapter&#xff1a;获取默认的蓝牙适配器。 getState&#xff1a;获取蓝…