🎯 贪心算法 — 跳跃游戏与区间调度

局部最优→全局最优,贪心是算法设计中最直觉也最需证明的策略

📖 贪心算法的核心思想

贪心算法在每一步都选择当前看起来最优的选项,期望局部最优选择能导致全局最优解。贪心的关键不是"贪",而是证明贪心策略的正确性。不是所有问题都能贪心,但能用贪心的问题往往有优美的解法。

贪心算法决策框架: 贪心可行吗? | +-- 贪心选择性质 --> 每步最优能导向全局最优? | | | +-- 是 --> 贪心可用! | +-- 最优子结构 --> 子问题的最优解能组成原问题最优解? | | | +-- 是 --> 配合贪心选择性质,贪心成立 | +-- 两者都不满足 --> 需要DP或回溯 常见贪心策略: +-- 排序贪心: 按某维度排序后依次选取 +-- 区间贪心: 按结束时间排序,选最早结束 +-- 跳跃贪心: 维护最远可达位置 +-- 分配贪心: 优先满足限制最紧的

1. 贪心 vs DP 深度对比

维度贪心DP
决策方式局部最优(不回头)穷举所有可能
正确性需证明(容易错)天然正确(定义保证)
时间复杂度通常O(n log n)通常O(n^2)或更高
适用范围窄(需特殊性质)宽(有最优子结构即可)
调试难度难(反例不明显)易(状态可追踪)

2. 贪心正确性证明方法

交换论证法(Exchange Argument):假设存在一个最优解不采用贪心策略,证明可以通过交换使得采用贪心策略的解不比最优解差。这是最常用的证明方法。

反证法:假设贪心策略不正确,推出矛盾。

构造法:直接构造出最优解,说明它就是贪心策略的输出。
面试策略:先想贪心(快),如果贪心不成立再转DP(稳)。能用贪心解决的问题,DP一定能解,但贪心更高效。面试中说出"为什么贪心正确"比写出代码更重要。

🔬 题目一:跳跃游戏(LC 55)

给定非负整数数组 nums,初始位于下标0,每个元素代表该位置可跳跃的最大步数,判断是否能到达最后一个下标。

思路分析

跳跃游戏贪心策略: nums = [2, 3, 1, 1, 4] 位置: 0 1 2 3 4 值: 2 3 1 1 4 i=0: max_reach = max(0, 0+2) = 2 i=1: max_reach = max(2, 1+3) = 4 --> 已能到终点 i=2: max_reach = max(4, 2+1) = 4 i=3: max_reach = max(4, 3+1) = 4 i=4: 到达终点! nums = [3, 2, 1, 0, 4] i=3: i=3 <= max_reach=3, max_reach=max(3,3+0)=3 i=4: i=4 > max_reach=3 --> 返回False
贪心策略:维护最远可达位置
1. max_reach 记录当前能到达的最远位置
2. 遍历每个位置i:如果 i > max_reach,说明到不了i,返回False
3. 否则更新 max_reach = max(max_reach, i + nums[i])
4. 遍历完没返回False,说明可达终点

代码实现

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        max_reach = 0
        for i, n in enumerate(nums):
            if i > max_reach:   # 到不了位置i
                return False
            max_reach = max(max_reach, i + n)
        return True

# 也可以提前终止
class Solution:
    def canJump(self, nums: List[int]) -> bool:
        max_reach = 0
        for i, n in enumerate(nums):
            if i > max_reach:
                return False
            max_reach = max(max_reach, i + n)
            if max_reach >= len(nums) - 1:
                return True
        return True

复杂度分析

时间: O(n) 空间: O(1)

🔬 题目二:跳跃游戏II(LC 45)

给定非负整数数组,假设总能到达最后一个下标,返回到达终点的最少跳跃次数。

思路分析

跳跃游戏II -- BFS层序扩展: nums = [2, 3, 1, 1, 4] 第0跳: 从0出发, 可达[1,2], farthest=3 第1跳: 从[1,2]出发, 可达[2,3,4], farthest=4 到达终点! 最少2跳 cur_end: 当前跳跃能到达的边界 farthest: 下一次跳跃能到达的最远位置 i=0: farthest=2, i==cur_end(0)-->跳! cur_end=2, jumps=1 i=1: farthest=4 i=2: farthest=4, i==cur_end(2)-->跳! cur_end=4, jumps=2
贪心策略:BFS层序扩展
把跳跃想象成BFS,每一"层"是一次跳跃能到达的范围:
1. cur_end表示当前步数能到达的最远边界
2. farthest记录下一步能到达的最远位置
3. 每当遍历到cur_end时,跳跃次数+1,cur_end更新为farthest
4. 注意:遍历到len(nums)-2即可(最后一步不需要跳)

代码实现

class Solution:
    def jump(self, nums: List[int]) -> int:
        jumps = 0
        cur_end = 0
        farthest = 0
        for i in range(len(nums) - 1):
            farthest = max(farthest, i + nums[i])
            if i == cur_end:
                jumps += 1
                cur_end = farthest
        return jumps

复杂度分析

时间: O(n) 空间: O(1)

跳跃游戏I判断"能不能到",跳跃游戏II求"最少几步到"。两题贪心策略一脉相承:维护最远可达范围。LC 45比LC 55难在需要精确控制"何时跳"的时机。

🔬 题目三:无重叠区间(LC 435)

给定区间集合,返回需要移除的最小区间数,使剩余区间互不重叠。

思路分析

区间调度贪心策略: 目标: 保留最多不重叠区间 --> 移除最少 按结束时间排序: 选最早结束的 --> 给后面留最多空间 原始区间: [1,2] [2,3] [3,4] [1,3] 按end排序: [1,2] [1,3] [2,3] [3,4] 选择过程: 选[1,2] --> end=2 [1,3] start=1 < end=2 --> 跳过(冲突) 选[2,3] start=2 >= end=2 --> end=3 选[3,4] start=3 >= end=3 --> end=4 保留3个,移除1个([1,3])
贪心策略:按结束时间排序,选最早结束的
1. 按区间结束时间升序排序
2. 选择第一个区间,记录其结束时间end
3. 遍历后续区间:若start >= end,选择并更新end;否则移除
4. 需要移除的数量 = 总数 - 保留数量

代码实现

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        intervals.sort(key=lambda x: x[1])
        count = 0
        end = intervals[0][1]
        for i in range(1, len(intervals)):
            if intervals[i][0] < end:  # 重叠
                count += 1
            else:
                end = intervals[i][1]
        return count

# 等价写法:求最多能保留多少个
class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        intervals.sort(key=lambda x: x[1])
        keep = 1
        end = intervals[0][1]
        for i in range(1, len(intervals)):
            if intervals[i][0] >= end:
                keep += 1
                end = intervals[i][1]
        return len(intervals) - keep

复杂度分析

时间: O(n log n)(排序)空间: O(1)

区间贪心有两种排序方式:按结束时间排序(选最多不重叠区间)vs 按开始时间排序(从右往左选)。面试中按结束时间排序更常见更直觉。注意边界条件:不同题目对端点重合的定义可能不同!

面试实战技巧

在实际面试中,算法题不仅仅是写代码,更考察沟通、分析和解决问题的能力。以下是一些实战技巧:

1. 解题步骤模板

5分钟分析:理解题意 - 确认边界 - 想暴力解 - 优化 - 写代码

1. 先复述题目,确认理解无误
2. 问清边界条件:空输入?负数?溢出?
3. 先说暴力解法(证明你理解问题)
4. 分析瓶颈,提出优化思路
5. 边写代码边解释思路

2. 常见陷阱

通用陷阱:
1. 索引越界:特别注意循环范围和数组边界
2. 初始化遗漏:DP的初始条件是否正确?
3. 遍历方向:依赖关系决定遍历顺序
4. 整数溢出:Python不溢出但其他语言注意
5. 边界条件:空数组、单元素、全相同

3. 代码模板

# 通用解题模板
class Solution:
    def solve(self, input_data):
        # 1. 边界检查
        if not input_data:
            return default_value
        # 2. 初始化
        result = initial_value
        state = initial_state
        # 3. 主循环
        for item in input_data:
            state = transition(state, item)
            result = max/min/update(result, state)
        # 4. 返回结果
        return result
面试中的代码不是越短越好,而是越清晰越好。适当的注释、合理的变量名、分步骤的实现,比一行式解法更能展示工程素养。
成就解锁:贪心猎手 — 掌握贪心算法的核心范式:跳跃游戏与区间调度
LeetCode AC验证:LC 55 Jump Game ✅ | LC 45 Jump Game II ✅ | LC 435 Non-overlapping Intervals ✅

📝 课后练习

  1. LC 121 买卖股票的最佳时机
  2. LC 122 买卖股票的最佳时机II
  3. LC 452 用最少数量的箭引爆气球
  4. LC 406 根据身高重建队列
  5. LC 763 划分字母区间

🔑 本课要点回顾

📚 扩展阅读

思考题:区间调度问题中,为什么按结束时间排序比按开始时间排序更优?