56. 合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
- 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
- 输出: [[1,6],[8,10],[15,18]]
- 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
- 输入: intervals = [[1,4],[4,5]]
- 输出: [[1,5]]
- 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
- 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。
所以一样的套路,先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。
按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1] 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=)
python
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.单调递增的数字
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
示例 1:
- 输入: N = 10
- 输出: 9
示例 2:
- 输入: N = 1234
- 输出: 1234
示例 3:
- 输入: N = 332
- 输出: 299
说明: N 是在 [0, 10^9] 范围内的一个整数。
本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。
想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。
最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。
python
class Solution:
def monotoneIncreasingDigits(self, n: int) -> int:
# 将整数转换为字符串
strNum = str(n)
# flag用来标记赋值9从哪里开始
# 设置为字符串长度,为了防止第二个for循环在flag没有被赋值的情况下执行
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)
968.监控二叉树 (可跳过)
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:

- 输入:[0,0,null,0,0]
- 输出:1
- 解释:如图所示,一台摄像头足以监控所有节点。
示例 2:

- 输入:[0,0,null,0,null,0,null,null,0]
- 输出:2
- 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
提示:
- 给定树的节点数的范围是 [1, 1000]。
- 每个节点的值都是 0。
所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!
局部最优推出全局最优,找不出反例,那么就按照贪心来!
此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
python
class Solution:
# Greedy Algo:
# 从下往上安装摄像头:跳过leaves这样安装数量最少,局部最优 -> 全局最优
# 先给leaves的父节点安装,然后每隔两层节点安装一个摄像头,直到Head
# 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
# 情况2:
# left == 0 && right == 0 左右节点无覆盖
# left == 1 && right == 0 左节点有摄像头,右节点无覆盖
# left == 0 && right == 1 左节点无覆盖,右节点有摄像头
# left == 0 && right == 2 左节点无覆盖,右节点覆盖
# left == 2 && right == 0 左节点覆盖,右节点无覆盖
if left == 0 or right == 0:
result[0] += 1
return 1
# 情况3:
# left == 1 && right == 2 左节点有摄像头,右节点有覆盖
# left == 2 && right == 1 左节点有覆盖,右节点有摄像头
# left == 1 && right == 1 左右节点都有摄像头
if left == 1 or right == 1:
return 2
1. 节点状态的定义
为了实现递归逻辑,我们给每个节点定义了三种逻辑状态:
-
状态 0:无覆盖 (Uncovered) ------ 这个节点需要被照看,但目前没被覆盖。
-
状态 1:有摄像头 (Camera) ------ 这个节点安装了摄像头。
-
状态 2:有覆盖 (Covered) ------ 这个节点虽然没装摄像头,但被它的子节点覆盖了。
2. 递归终止条件的妙处
if not cur:
return 2
为什么要返回 2(有覆盖)?
这是为了"贪心"。如果我们返回 0(无覆盖),那么叶子节点就会觉得自己没被覆盖,从而被迫在叶子节点装摄像头;如果我们返回 1(有摄像头),那叶子节点就变成了"被摄像头覆盖"的状态。
只有返回 2,叶子节点才会把压力传给它的父节点,让父节点去装摄像头。
3. 三种核心情况的判断
递归是自底向上的(后序遍历),我们根据左右孩子的状态来决定当前节点要做什么:
情况 1:左右孩子都有覆盖 (left == 2 and right == 2)
-
逻辑:既然左右孩子都已经被照顾好了,当前节点就没必要装摄像头。
-
决策:为了省摄像头,当前节点先不装,告诉父节点:"我还没被覆盖,你看着办。"
-
返回 :
0(无覆盖)。
情况 2:左右孩子中至少有一个没被覆盖 (left == 0 or right == 0)
-
逻辑 :只要有一个孩子是"裸奔"状态(状态 0),当前节点必须装摄像头来救它,否则就没机会了。
-
决策 :
result[0] += 1(安装一个摄像头)。 -
返回 :
1(有摄像头)。
情况 3:左右孩子中至少有一个有摄像头 (left == 1 or right == 1)
-
逻辑:只要孩子里有一个装了摄像头,当前节点就被覆盖到了。
-
决策:不需要重复装摄像头。
-
返回 :
2(有覆盖)。
4. 根节点的特殊处理
if self.traversal(root, result) == 0:
result[0] += 1
递归结束后,如果根节点返回的状态是 0(无覆盖),说明根节点的上方没有人能救它了(没有父节点了)。
此时,我们必须在根节点自己身上强行安装一个摄像头。
总结与性能分析
-
贪心策略:从下往上,跳过叶子,在叶子的父节点放摄像头,每隔两层放一个。
-
时间复杂度:O(n)。每个节点只被访问一次。
-
空间复杂度:O(h)。h 是树的高度,主要开销是递归调用的栈空间。