算法训练营 Day31 - 贪心算法 Part05

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 是树的高度,主要开销是递归调用的栈空间。

相关推荐
锅包一切2 小时前
PART2 双指针
c++·算法·leetcode·力扣·双指针
tankeven2 小时前
HJ91 走方格的方案数
c++·算法
俩娃妈教编程2 小时前
2024 年 09 月 二级真题(2)--小杨的矩阵
c++·算法·gesp真题
浅念-2 小时前
C++ STL vector
java·开发语言·c++·经验分享·笔记·学习·算法
小雨中_2 小时前
2.8 策略梯度(Policy Gradient)算法 与 Actor-critic算法
人工智能·python·深度学习·算法·机器学习
m0_531237172 小时前
C语言-if/else,switch/case
c语言·数据结构·算法
Hag_202 小时前
LeetCode Hot100 239.滑动窗口最大值
数据结构·算法·leetcode
漂流瓶jz2 小时前
UVA-1604 立体八数码问题 题解答案代码 算法竞赛入门经典第二版
算法·ida·深度优先·图论·dfs·bfs·迭代加深搜索
m0_531237172 小时前
C语言-while循环,continue/break,getchar()/putchar()
java·c语言·算法