代码随想录算法训练营 DAY30|贪心算法4

今天,我迎来了贪心算法章节的最后一部分——重叠区间问题。这几道题都有一个共同的特点:初看时觉得条件复杂,不知从何下手,但一旦看到贪心解法,便会忍不住惊呼:“原来可以这么巧妙!”

这正印证了我对贪心算法的印象:它是一种“经验学科”。没做过,可能绞尽脑汁也想不出来;但只要做过一次,理解了其中的贪心策略,这类问题就变得清晰明了。今天的三道题,让我对处理区间问题的信心大增。

一、实战演练:在区间中寻找最优解

1. LeetCode 452. 用最少数量的箭引爆气球

题目描述: 在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标 [x_start, x_end]。一支弓箭可以从 x 轴的任意点垂直向上射出。如果一支箭的坐标为 x,那么它可以引爆所有满足 x_start ≤ x ≤ x_end 的气球。请求出引爆所有气球所需要的最小弓箭数。

  • 学习感悟: 这个问题的本质是,找到最少的点,能够覆盖所有的区间。我的贪心策略是:优先处理结束坐标早的气球
    1. 排序:将所有气球按结束坐标 x_end 升序排列。
    2. 射箭:选择第一个气球的结束点作为第一支箭的射出位置。这支箭必然能引爆第一个气球,并且,这是能覆盖到第一个气球的“最靠右”的位置,因此它有最大的可能性去覆盖后续的气球。
    3. 遍历:遍历剩下的气球。如果某个气球的开始坐标 x_start 在我当前这支箭的位置之后,说明这支箭已经够不着它了,我必须再射一支新的箭。新箭的位置则更新为这个新气球的结束坐标。
  • 资源包 (题目/文章/视频): 代码随想录-452.用最少数量的箭引爆气球
    在这里插入图片描述
我的实现:按区间结尾排序
class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        if not points:
            return 0
        
        # 关键:按区间的结束位置升序排序
        points.sort(key=lambda x: x[1])
        
        arrows = 1
        # 第一支箭射在第一个气球的末端
        arrow_pos = points[0][1]
        
        for i in range(1, len(points)):
            # 如果当前气球的开始位置在前一支箭的范围之外
            if points[i][0] > arrow_pos:
                # 需要一支新的箭
                arrows += 1
                # 更新新箭的位置为当前气球的末端
                arrow_pos = points[i][1]
                
        return arrows
2. LeetCode 435. 无重叠区间

题目描述: 给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

  • 学习感悟: 这道题可以反过来思考:要移除最少的区间,就等同于保留最多的无重叠区间。这样问题就转化成了“最多能选出多少个互不重叠的区间”。贪心策略与上一题非常相似:
    1. 排序:仍然是按区间的结束位置 end 升序排列。
    2. 选择:首先选择第一个区间(它结束得最早)。然后遍历剩余的区间,如果某个区间的开始位置 start 大于或等于我上一个所选区间的结束位置,说明它们不重叠,我就可以选择这个新区间,并更新我的“末尾边界”。
      为什么这样是对的?因为每次都选择结束最早的那个区间,就能给后面的区间留出尽可能多的可用空间。
  • 资源包 (题目/文章/视频): 代码随想录-435.无重叠区间
我的实现:计算最多可保留的区间数
class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        
        # 按结束位置升序排序
        intervals.sort(key=lambda x: x[1])
        
        kept_count = 1 # 至少保留第一个区间
        last_end = intervals[0][1]
        
        for i in range(1, len(intervals)):
            # 如果当前区间的开始不早于上一个保留区间的结束
            if intervals[i][0] >= last_end:
                # 保留这个区间
                kept_count += 1
                last_end = intervals[i][1]
                
        # 总数减去保留数,就是需要移除的数
        return len(intervals) - kept_count
3. LeetCode 763. 划分字母区间

题目描述: 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

  • 学习感悟: 这道题的“区间”是隐式的,由每个字母的第一次和最后一次出现的位置决定。贪心策略是:尽可能让第一个片段最短
    1. 预处理:首先遍历一遍字符串,用一个哈希表记录下每个字母最后出现的位置。
    2. 划分:再次遍历字符串,维护一个当前片段的“最远边界”end。对于遍历到的每一个字符,我都去哈希表里查询它的最远出现位置,并用这个位置来更新 end。当我的遍历指针 i 恰好走到了 end 位置时,说明从 startend 这个片段里的所有字符,其最远出现位置都不会超过 end。此时,一个完美的片段就形成了。我立刻记录下这个片段的长度,并更新下一个片段的 start
  • 资源包 (题目/文章/视频): 代码随想录-763.划分字母区间
我的实现:双指针 + 哈希表
class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        # 预处理,记录每个字符的最后出现位置
        last_occurrence = {char: i for i, char in enumerate(s)}
        
        result = []
        start, end = 0, 0
        
        for i, char in enumerate(s):
            # 更新当前分区的最远边界
            end = max(end, last_occurrence[char])
            
            # 如果当前位置 i 已经到达了最远边界
            if i == end:
                # 找到了一个完整的片段
                length = end - start + 1
                result.append(length)
                # 更新下一个片段的起始位置
                start = i + 1
                
        return result

总结

贪心算法的区间问题,核心就在于排序。通过一个巧妙的排序规则(通常是按区间结尾),可以将一个复杂的问题转化为一个简单的线性扫描问题。今天的三道题让我对这个模式有了非常深刻的理解。至此,整个贪心算法章节的学习就告一段落了。虽然它没有固定的模板,但其中蕴含的解题智慧,如“局部最优”、“解耦约束”、“排序简化问题”等,都是非常宝贵的财富。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值