【Hot100】11.贪心算法

这一类的核心思想是:在每一步选择中都采取在当前状态下最好/最优的选择(局部最优),从而希望导致结果是全局最优的。

  • 适用场景:
    • 问题可以分解为多个步骤,且每一步的选择互不干扰(或干扰可被局部最优覆盖)。
    • 具有“贪心选择性质”和“最优子结构”。
    • 常见于:区间问题、分配问题、跳跃问题、霍夫曼编码等。
  • 解题关键:
    • 证明难,直觉准:贪心算法往往很难严格证明,但在面试中,通常通过“反证法”或“举反例”来验证直觉。如果找不到反例,且逻辑自洽,通常就是贪心。
    • 维护关键变量:如 maxReach(最远可达位置)、end(当前段结束位置)、lastPos(字符最后出现位置)等。

📝 本类题型总结

题目 核心技巧 关键差异点 (vs 跳跃游戏)
55. 跳跃游戏 最远可达范围 基准:维护 maxReach,判断 i > maxReach。
45. 跳跃游戏 II 边界计数 增加 end 边界。i == end 时步数 +1,更新 end = maxReach。求最小步数。
763. 划分字母区间 最后位置 + 边界 预处理 lastPos。end = max(end, lastPos)。i == end 时切割。求片段长度。

🏆 母题:LeetCode 55. 跳跃游戏 (Jump Game)

题目内容:给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
核心逻辑:

  1. 局部最优:在每一步,都尝试跳到能跳到的最远位置。
  2. 全局判断:维护一个 maxReach 变量,表示当前能到达的最远下标。
  3. 失败条件:如果遍历到某个下标 i,而 i > maxReach,说明前面的所有跳跃都无法到达 i,直接返回 false。
  4. 成功条件:如果 maxReach >= 最后一个下标,返回 true。
class Solution {
    public boolean canJump(int[] nums) {
        int maxReach = 0; // ⚠️ [核心变量] 当前能到达的最远下标
        
        for (int i = 0; i < nums.length; i++) {
            // ⚠️ [核心逻辑 1] 如果当前位置超过了最远可达范围,说明无法到达
            if (i > maxReach) {
                return false;
            }
            
            // ⚠️ [核心逻辑 2] 更新最远可达范围
            maxReach = Math.max(maxReach, i + nums[i]);
            
            // ⚠️ [剪枝] 如果已经能到达或超过终点,提前返回
            if (maxReach >= nums.length - 1) {
                return true;
            }
        }
        
        return true;
    }
}

🔄 变体 1:LeetCode 45. 跳跃游戏 II (Jump Game II)

题目内容:给定一个长度为 n 的非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。
相比母题的变化:

  1. 目标变化:从“能否到达”变为“最少步数”。
  2. 边界控制:
    • maxReach:当前能跳到的最远位置(同母题)。
    • end:当前这一步能跳到的最远边界。
    • steps:跳跃次数。
  3. 贪心策略:
    • 在 [start, end] 范围内遍历,不断更新 maxReach。
    • 当遍历到 i == end 时(说明当前这一步的所有可能性都探索完了),必须进行一次跳跃,步数 steps++,并将 end 更新为 maxReach(进入下一步的范围)。
    • 注意:不需要遍历到最后一个元素,因为到达最后一个元素前的边界时,就已经确定能到了。
class Solution {
    public int jump(int[] nums) {
        if (nums.length <= 1) return 0;
        
        int steps = 0;
        int maxReach = 0;
        int end = 0; // ⚠️ [变化点 1] 当前步数能到达的边界
        
        // ⚠️ [变化点 2] 不需要遍历到最后一个元素,因为到达倒数第二个元素的边界时,下一步肯定能到终点
        for (int i = 0; i < nums.length - 1; i++) {
            maxReach = Math.max(maxReach, i + nums[i]);
            
            // ⚠️ [变化点 3] 到达当前步的边界,必须跳跃
            if (i == end) {
                steps++;
                end = maxReach; // 更新边界为下一步能到的最远位置
                
                // 优化:如果更新后的边界已经覆盖终点,可以直接返回
                if (end >= nums.length - 1) {
                    return steps;
                }
            }
        }
        
        return steps;
    }
}

🔄 变体 2:LeetCode 763. 划分字母区间 (Partition Labels)

题目内容:给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
相比母题的变化:

  1. 预处理:需要先遍历一次字符串,记录每个字符最后出现的位置 lastPos[char]。
  2. 贪心策略:
    • 遍历字符串,维护当前片段的结束位置 end。
    • 对于当前字符 c,其最后出现位置是 lastPos[c]。为了保证 c 不出现在其他片段,当前片段必须至少延伸到 lastPos[c]。
    • 所以:end = max(end, lastPos[c])。
    • 分割点:当遍历下标 i == end 时,说明当前片段内所有字符的最后出现位置都在 i 之前(或就是 i),可以安全切割。
import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<Integer> partitionLabels(String s) {
        List<Integer> result = new ArrayList<>();
        int[] lastPos = new int[26];
        
        // ⚠️ [变化点 1] 预处理:记录每个字符最后出现的位置
        for (int i = 0; i < s.length(); i++) {
            lastPos[s.charAt(i) - 'a'] = i;
        }
        
        int start = 0;
        int end = 0;
        
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            // ⚠️ [变化点 2] 更新当前片段必须延伸到的最远位置
            end = Math.max(end, lastPos[c - 'a']);
            
            // ⚠️ [变化点 3] 贪心分割:当前位置等于最远位置,说明可以切割
            if (i == end) {
                result.add(end - start + 1);
                start = i + 1; // 更新下一片段的起点
            }
        }
        
        return result;
    }
}
posted @ 2026-03-08 22:00  scales123  阅读(42)  评论(0)    收藏  举报