435. 无重叠区间
文档讲解:代码随想录
题目链接:. - 力扣(LeetCode)
本道题与上个题目相似,都是求重叠区间
统计重叠区间的个数,减去重叠区间的个数就是无重叠区间了
主要就是为了让区间尽可能的重叠。(为什么)
按照左边界排序
①如果i的左边界大于等于上一个区间的右边界,就没有重叠
②如果i的左边界小于上一个区间的右边界,就重叠了,统计重叠区间
下一个区间是否与上两个区间重叠呢?更新右边界,如果下一个区间的左边界小于新的右边界,那么与上面两个都重叠了,也要统计一下,如果大于新的右边界,因为上面我们会将重叠的较大的右边界区间移除,这时候就没有重叠的区间了
class Solution:# 定义一个方法,用于计算移除重叠区间的最小数量def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:# 初始化结果计数器为 0result = 0# 如果区间列表为空,直接返回 0if 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]:continueelse:# 否则,存在重叠,需要移除一个区间,结果计数器加 1result += 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'))] = ileft = 0right = 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+1return 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)
这道题目首先要想,如何放置,才能让摄像头最小的呢?
从题目中示例,其实可以得到启发,我们发现题目示例中的摄像头都没有放在叶子节点上!
这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。
所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。
那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢?
因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。
所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!
局部最优推出全局最优,找不出反例,那么就按照贪心来!
此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
此时这道题目还有两个难点:
- 二叉树的遍历
- 如何隔两个节点放一个摄像头
#确定遍历顺序
在二叉树中如何从低向上推导呢?
可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。
如何隔两个节点放一个摄像头
此时需要状态转移的公式,大家不要和动态的状态转移公式混到一起,本题状态转移没有择优的过程,就是单纯的状态转移!
来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:
有如下三种:
- 该节点无覆盖
- 本节点有摄像头
- 本节点有覆盖
我们分别有三个数字来表示:
- 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 = 0def traversal(cur):#遇到空节点返回有覆盖的状态nonlocal resultif not cur:return 2left = traversal(cur.left)right = traversal(cur.right)##递推关系0:无覆盖,1:有摄像头 2:有覆盖#①左右节点都有覆盖(都是状态2),父节点无覆盖if left==right==2:return 0 ##②只要有一个无覆盖,父节点就要有摄像头elif left==0 or right==0:result += 1return 1##③至少有一个有摄像头,父节点就是有覆盖elif left==1 or right==1:return 2#根节点如果无覆盖就再加一个摄像头if traversal(root) ==0:result += 1return result