代码随想录训练营 Day31打卡 贪心算法 part05
一、 力扣56. 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 :
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。
按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1] 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=)
如图,(注意图中区间都是按照左边界排序之后了):
知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢?
其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。
代码实现
class Solution:
def merge(self, intervals):
result = [] # 用于存放合并后的区间结果
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 result[-1][1] >= intervals[i][0]:
# 合并区间:更新结果集中最后一个区间的右边界为当前区间和结果集中右边界的较大值
result[-1][1] = max(result[-1][1], intervals[i][1])
else:
# 如果当前区间与结果集中最后一个区间不重叠,直接将当前区间加入结果集
result.append(intervals[i])
return result # 返回合并后的区间结果集
力扣题目链接
题目文章讲解
题目视频讲解
二、 力扣738. 单调递增的数字
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
示例 1:
输入: n = 10
输出: 9
示例 2:
输入: n = 1234
输出: 1234
题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。
例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]–,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。
从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299
版本一
将输入整数 N 转换为字符串 strNum,方便逐位操作。
从右向左遍历字符串,检查每一位数字与前一位的关系。
如果发现当前位数字小于前一位,说明递增性被破坏,更新 flag 为当前的位置,并将前一位数字减1以修复递增性。
将从 flag 开始的所有位置上的数字改为9,确保生成的数字是最大且递增的。
最终,将修改后的字符串转换为整数返回。
class Solution:
def monotoneIncreasingDigits(self, N: int) -> int:
# 将整数N转换为字符串形式,方便逐位处理
strNum = str(N)
# flag用于标记从哪里开始将数字修改为9,初始值设置为字符串的长度,避免错误的更新
flag = len(strNum)
# 从右往左遍历字符串中的每一位
for i in range(len(strNum) - 1, 0, -1):
# 如果当前位的数字比前一位的数字小,说明递增性被破坏,需要调整
if strNum[i - 1] > strNum[i]:
flag = i # 更新flag,记录需要修改的位置
# 将前一位数字减1,并更新字符串
strNum = strNum[:i - 1] + str(int(strNum[i - 1]) - 1) + strNum[i:]
# 从flag位置开始,将其后的所有数字都变为9,以保证生成的数字最大且递增
for i in range(flag, len(strNum)):
strNum = strNum[:i] + '9' + strNum[i + 1:]
# 最后将字符串转换回整数返回
return int(strNum)
版本二
将输入整数 N 转换为字符列表 strNum,便于逐位修改。
如果当前位数字比前一位数字小,递增性被破坏。
将前一位数字减1,并将当前位置及其后面的数字全都设为9,保证最大化。
最后将字符列表转换为字符串,再转换为整数返回。
class Solution:
def monotoneIncreasingDigits(self, N: int) -> int:
# 将整数N转换为字符列表,便于逐位操作
strNum = list(str(N))
# 从右向左遍历字符串中的每一位
for i in range(len(strNum) - 1, 0, -1):
# 如果当前位数字比前一位数字小,说明递增性被破坏,需要调整
if strNum[i - 1] > strNum[i]:
# 将前一位数字减1
strNum[i - 1] = str(int(strNum[i - 1]) - 1)
# 将当前位置及其之后的所有数字都设为9,确保递增性并最大化
for j in range(i, len(strNum)):
strNum[j] = '9'
# 将字符列表转换回字符串,再转换为整数返回
return int(''.join(strNum))
版本三 贪心法 精简版
将整数 N 转换为字符串 strNum。
找到递增性被破坏的位置,如果找到,将前一位数字减1,后续所有位置设为9。
将字符串 strNum 转换为整数并返回。
class Solution:
def monotoneIncreasingDigits(self, N: int) -> int:
# 将整数N转换为字符串形式
strNum = str(N)
# 从右向左遍历字符串中的每一位
for i in range(len(strNum) - 1, 0, -1):
# 如果当前位数字比前一位数字小,递增性被破坏
if strNum[i - 1] > strNum[i]:
# 将前一位数字减1,并将当前位置及之后的所有数字设为9
strNum = strNum[:i - 1] + str(int(strNum[i - 1]) - 1) + '9' * (len(strNum) - i)
# 将最终的字符串转换为整数并返回
return int(strNum)
力扣题目链接
题目文章讲解
题目视频讲解
三、 力扣968. 监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视 其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。
示例 2:
输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
本题我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!
如何隔两个节点放一个摄像头
来看看这个状态应该如何转移,先来看看每个节点可能有几种状态,我们分别有三个数字来表示:
0:该节点无覆盖
1:本节点有摄像头
2:本节点有覆盖
空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了
主要有如下四类情况:
情况1:左右节点都有覆盖
左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。
情况2:左右节点至少有一个无覆盖的情况
这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。
此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。
情况3:左右节点至少有一个有摄像头
如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态)
情况4:头结点没有覆盖
以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况:
所以递归结束之后,还要判断根节点,如果没有覆盖,result++
代码实现
class Solution:
# 贪心算法:从下往上安装摄像头,以确保安装数量最少,达到局部最优从而实现全局最优
# 主要思路是跳过叶子节点,优先给叶子节点的父节点安装摄像头,并且每隔两层节点安装一个摄像头,直到根节点
# 节点状态定义:
# 0: 该节点未覆盖(需要安装摄像头)
# 1: 该节点有摄像头
# 2: 该节点已被覆盖(由子节点的摄像头覆盖)
def minCameraCover(self, root: TreeNode) -> int:
result = [0] # 用于记录摄像头的安装数量,使用列表是为了在递归中修改该值
# 调用递归函数,如果根节点未被覆盖,则需要在根节点安装一个摄像头
if self.traversal(root, result) == 0:
result[0] += 1
return result[0] # 返回最终安装的摄像头数量
def traversal(self, cur: TreeNode, result: List[int]) -> int:
if not cur: # 如果当前节点为空,表示这个节点已被视为已覆盖(不需要安装摄像头)
return 2
# 递归处理当前节点的左子树
left = self.traversal(cur.left, result)
# 递归处理当前节点的右子树
right = self.traversal(cur.right, result)
# 情况1: 左右节点都有覆盖(但当前节点本身未被覆盖)
# 如果左右子节点都处于已覆盖状态,但没有摄像头,当前节点需要摄像头来覆盖
if left == 2 and right == 2:
return 0 # 当前节点未被覆盖,返回0
# 情况2: 当前节点的左右子节点至少有一个未被覆盖
# 如果任意一个子节点未被覆盖,则当前节点需要安装摄像头
elif left == 0 or right == 0:
result[0] += 1 # 增加摄像头数量
return 1 # 当前节点安装了摄像头,返回1
# 情况3: 当前节点的子节点有一个摄像头覆盖,或者两个子节点都有摄像头覆盖
# 在这种情况下,当前节点已经被覆盖,不需要额外安装摄像头
else:
return 2 # 当前节点已被覆盖,返回2
力扣题目链接
题目文章讲解
题目视频讲解