首页 > 编程语言 >代码随想录算法训练营第三十七天|435. 无重叠区间、763.划分字母区间、56. 合并区间、738.单调递增的数字、968.监控二叉树

代码随想录算法训练营第三十七天|435. 无重叠区间、763.划分字母区间、56. 合并区间、738.单调递增的数字、968.监控二叉树

时间:2024-05-25 20:59:34浏览次数:33  
标签:right 覆盖 随想录 result 区间 节点 摄像头 第三十七

435. 无重叠区间

文档讲解:代码随想录

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

本道题与上个题目相似,都是求重叠区间

统计重叠区间的个数,减去重叠区间的个数就是无重叠区间了

主要就是为了让区间尽可能的重叠。(为什么)

按照左边界排序

①如果i的左边界大于等于上一个区间的右边界,就没有重叠

如果i的左边界小于上一个区间的右边界,就重叠了,统计重叠区间

下一个区间是否与上两个区间重叠呢?更新右边界,如果下一个区间的左边界小于新的右边界,那么与上面两个都重叠了,也要统计一下,如果大于新的右边界,因为上面我们会将重叠的较大的右边界区间移除,这时候就没有重叠的区间了

class Solution:
    # 定义一个方法,用于计算移除重叠区间的最小数量
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        # 初始化结果计数器为 0
        result = 0
        # 如果区间列表为空,直接返回 0
        if len(intervals) == 0:
            return 0
        # 按区间的起始位置对区间列表进行排序
        intervals.sort(key = lambda x: x[0])
        # 遍历区间列表,从第二个区间开始
        for i in range(1, len(intervals)):
            # 如果当前区间的起始位置大于等于前一个区间的结束位置,则没有重叠
            if intervals[i][0] >= intervals[i-1][1]:
                continue
            else:
                # 否则,存在重叠,需要移除一个区间,结果计数器加 1
                result += 1
                # 更新当前区间的结束位置为当前区间和前一个区间结束位置的较小值
                intervals[i][1] = min(intervals[i][1], intervals[i-1][1])
        # 返回需要移除的区间数量
        return result

763.划分字母区间

文档讲解:代码随想录

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

要求:①划分为尽可能多的片段②同一字母最多出现在一个片段中

解题思路:记录每个字母的最远处位置,然后从0开始遍历字母到最远处字母出现的位置,如果还没有遍历到最远处,就已经出现了更远的字母,那么就要更新最远处下标,直到遍历结束

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

可以分为如下两步:

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

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        index = [0]*26 #记录每个字母最远出现的位置
        result = []
        for i in range(len(list(s))):
            index[(ord(s[i])-ord('a'))] = i
        
        left = 0
        right = 0
        ##for循环不好写,看卡哥写的很巧妙
        for i in range(len(list(s))):
            right = max(index[(ord(s[i])-ord('a'))],right) #更新最远处位置
            if i==right:#关键:如何判断已经到了最远处
                result.append(right-left+1)
                left = right+1
        return result

56. 合并区间

文档讲解:代码随想录

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

问题

如果按照求重叠区间的方法做,判断出来重叠区间后,如何更新新的开始和结束区间(合并区间)?

找到重叠区间的方法可以按照前面讲的,但是如果有三个重叠的,该怎么合并区间呢

解答:直接将上一个元素放入result数组中,每次取出result数组的最后一个进行比较即可,有重叠则修改result数组最后一个元素,没有重叠则直接加入result数组

易错点:需要比较右区间的大小,更新右区间

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        if len(intervals) ==0:
            return  
        result = []
        #先排序,左边界由小到大
        intervals.sort(key = lambda x :x[0])
        #!关键!先将第一个放入区间中,方便后面的操作
        result.append(intervals[0])
        for i in range(1,len(intervals)):
            #没有重叠,直接加入数组
            if intervals[i][0] > result[-1][1]:
                result.append(intervals[i])
            #有重合,合并区间
            else:
                #右边界的大小不确定,需要取最大值
                result[-1] = [result[-1][0],max(intervals[i][1],result[-1][1])]
        return result

总结:

本题的本质其实还是判断重叠区间问题。

大家如果认真做题的话,会发现和我们刚刚讲过的452. 用最少数量的箭引爆气球 (opens new window)和 435. 无重叠区间 (opens new window)都是一个套路。

这几道题都是判断区间重叠,区别就是判断区间重叠后的逻辑,本题是判断区间重叠后要进行区间合并。

所以一样的套路,先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。

按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1] 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=)

738.单调递增的数字

文档讲解:代码随想录

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

题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。

例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]--,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。

这一点如果想清楚了,这道题就好办了。

此时是从前向后遍历还是从后向前遍历呢?

从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。

这么说有点抽象,举个例子,数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。

那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299

确定了遍历顺序之后,那么此时局部最优就可以推出全局,找不出反例,试试贪心。

当不确定遍历顺序时,就手动模拟一下

class Solution:
    def monotoneIncreasingDigits(self, n: int) -> int:
        s = str(n)
        flag = len(s)#flag往后的数值都是9  如果本来就是递增的,则flag不起作用,所以初始值为数组的长度
        print(flag)
        for i in range(len(list(s))-1,0,-1):
            #当前面一位大于后面一位
            if s[i-1] > s[i]:
                # s[i-1] = str(int(s[i-1])-1) #不可以这样写,符串是不可变的(immutable)对象,因此你不能通过索引来修改字符串中的单个字符
                s = s[:i-1] + str(int(s[i-1])-1) + s[i:]
                flag = i #避免1000得到900的情况
            for i in range(flag,len(s)):
                s = s[:i] + '9' + s[i+1:]
        return int(s)

总结 

本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。

想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。

最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。避免1000得到900的情况

968.监控二叉树

困难

文档讲解:代码随想录

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

这道题目首先要想,如何放置,才能让摄像头最小的呢?

从题目中示例,其实可以得到启发,我们发现题目示例中的摄像头都没有放在叶子节点上!

这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。

所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。

那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢?

因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。

所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!

局部最优推出全局最优,找不出反例,那么就按照贪心来!

此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

此时这道题目还有两个难点:

  1. 二叉树的遍历
  2. 如何隔两个节点放一个摄像头

#确定遍历顺序

在二叉树中如何从低向上推导呢?

可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。

如何隔两个节点放一个摄像头

此时需要状态转移的公式,大家不要和动态的状态转移公式混到一起,本题状态转移没有择优的过程,就是单纯的状态转移!

来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:

有如下三种:

  • 该节点无覆盖
  • 本节点有摄像头
  • 本节点有覆盖

我们分别有三个数字来表示:

  • 0:该节点无覆盖
  • 1:本节点有摄像头
  • 2:本节点有覆盖

大家应该找不出第四个节点的状态了。

一些同学可能会想有没有第四种状态:本节点无摄像头,其实无摄像头就是 无覆盖 或者 有覆盖的状态,所以一共还是三个状态。

因为在遍历树的过程中,就会遇到空节点,那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢?

回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。

那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。

所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了

接下来就是递推关系。

那么递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖),原因上面已经解释过了。

归的函数,以及终止条件已经确定了,再来看单层逻辑处理。

主要有如下四类情况:

  • 情况1:左右节点都有覆盖

左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。

如图:

  • 情况2:左右节点至少有一个无覆盖的情况

如果是以下情况,则中间节点(父节点)应该放摄像头:

  • left == 0 && right == 0 左右节点无覆盖
  • left == 1 && right == 0 左节点有摄像头,右节点无覆盖
  • left == 0 && right == 1 左节点有无覆盖,右节点摄像头
  • left == 0 && right == 2 左节点无覆盖,右节点覆盖
  • left == 2 && right == 0 左节点覆盖,右节点无覆盖

这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。

此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。

  • 情况3:左右节点至少有一个有摄像头

如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态)

  • left == 1 && right == 2 左节点有摄像头,右节点有覆盖
  • left == 2 && right == 1 左节点有覆盖,右节点有摄像头
  • left == 1 && right == 1 左右节点都有摄像头

  • 情况4:头结点没有覆盖

以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图:

 以递归结束之后,还要判断根节点,如果没有覆盖,result++

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def minCameraCover(self, root: Optional[TreeNode]) -> int:
        result = 0
        def traversal(cur):
            #遇到空节点返回有覆盖的状态
            nonlocal result
            if not cur:
                return 2
            left = traversal(cur.left)
            right = traversal(cur.right)
            ##递推关系0:无覆盖,1:有摄像头 2:有覆盖
            #①左右节点都有覆盖(都是状态2),父节点无覆盖
            if left==right==2:
                return 0 
            ##②只要有一个无覆盖,父节点就要有摄像头
            elif left==0 or right==0:
                result += 1
                return 1
            ##③至少有一个有摄像头,父节点就是有覆盖
            elif left==1 or right==1:
                return 2
        #根节点如果无覆盖就再加一个摄像头
        if traversal(root) ==0:
            result += 1
        return result

标签:right,覆盖,随想录,result,区间,节点,摄像头,第三十七
From: https://blog.csdn.net/qq_52149213/article/details/139182745

相关文章

  • 代码随想录算法训练营第十六天 | 104.二叉树的最大深度、559.n叉树的最大深度、111.二
    104.二叉树的最大深度题目链接:https://leetcode.cn/problems/maximum-depth-of-binary-tree/文档讲解:https://programmercarl.com/0104.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE......
  • 代码随想录算法训练营第36期DAY38
    DAY38435无重叠区间昨晚很快就想出来了,今天相当于二刷。class Solution {public:    static bool mycmp(vector<int>&a,vector<int>&b){        return a[1]<b[1];    }    int eraseOverlapIntervals(vector<vector<int>>& intervals) {   ......
  • 代码随想录算法训练营第36期DAY39
    道心破碎的一天,继续加油吧,坚持努力。DAY39738单调递增的数字暴力法:没有想到用inti=n;i>0;i--来遍历。class Solution {private:    bool checknum(int num){        if(num<10) return true;        while(num/10!=0){           ......
  • 代码随想录算法训练营第36期DAY37
    DAY37先二刷昨天的3道题目,每种方法都写:是否已完成:是。报告:134加油站的朴素法没写对。原因是:在if中缺少了store>=0的判断,只给出了index==i的判断。前进法没写出来。因为忘记了总油量的判断。Sum。注意变量的初始化。分配糖果注意if里面放的是ratings;860柠檬水找零网上摘得思......
  • 线段树维护区间字符的两道例题(CF240F CF558E)
    CF240F:https://www.luogu.com.cn/problem/CF240F题目大意:给定一个长为n的由a到z组成的字符串,有m次操作,每次操作将[l,r]的字符串进行重排,得到字典序最小的字符串,输出m次操作后的字符串。大致思路:1.首先我们要想区间内的字典序最小的回文串要怎么构造。回文串无非就两种类型:有一......
  • 代码随想录——二叉树的所有路径(Leetcode257)需要回顾
    题目链接BFS+队列维护一个队列,存储节点以及根到该节点的路径。一开始这个队列里只有根节点。在每一步迭代中,我们取出队列中的首节点,如果它是叶子节点,则将它对应的路径加入到答案中。如果它不是叶子节点,则将它的所有孩子节点加入到队列的末尾。当队列为空时广度优先搜......
  • 区间DP
    区间DP区间DP的一般表达式:枚举区间长度枚举区间起点计算区间终点枚举区间断点P1220关路灯状态dp[i][j][0/1]表示区间[i,j]的路灯全关掉且站在i或j处的最小功耗答案min(dp[1][n][0],dp[1][n][1])状态转移for(intlen=2;len<=n;len++){ for(inti=1;i+len-1<=n;i++){......
  • 【知识点】浅入线段树与区间最值问题
    前言:这又是一篇关于数据结构的文章。今天来讲一下线段树和线段树的基本应用。线段树(SegmentTree),是一种非常高效且高级的数据结构,其主要用于区间查询和与区间更新相关的问题,例如进行多次查询区间最大值、最小值、更新区间等操作。区间最值问题引入常见的线段树题型就是区......
  • 代码随想录算法训练营第一天 | 977.有序数组的平方;
    代码随想录算法训练营第一天|977.有序数组的平方;977题链接:https://leetcode.cn/problems/squares-of-a-sorted-array/代码随想录链接:https://programmercarl.com/0977.有序数组的平方.html#思路209题链接:https://leetcode.cn/problems/minimum-size-subarray-sum/submission......
  • 代码随想录算法训练营第第17天 | 110.平衡二叉树、257. 二叉树的所有路径、404.左叶子
    三道题都没想出来,还是要加强递归的练习110.平衡二叉树(优先掌握递归)再一次涉及到,什么是高度,什么是深度,可以巩固一下。题目链接/文章讲解/视频讲解:https://programmercarl.com/0110.平衡二叉树.htmlfunctiongetHeight(node){if(node===null)return0;letleftH......