[算法手记] 贪心 爬楼梯问题

本文将用于分析博主的算法刷题记录,同时也是为了勉励自己每天刷题保持手感,记录博客

这次要介绍的是两道力扣经典贪心算法题

一, 跳跃游戏

附上题目链接

https://leetcode.cn/problems/jump-game/description/

1.思路

题目很简单,让我们找出能否到达楼梯的最顶部

本题我们就使用贪心来解决,关键点在于维护一个可以到达的最远跳跃视野maxLen.

maxLen是变化的, 每走到一个位置,它都会赋予我们一个新的"最远跳跃视野"。

当我们位于i处的台阶上, 台阶上表示的数字代表我们可以跳跃的最远距离,随着我们的跳跃,可能会出现一个问题,

  1. 当我们所处位置 i < maxLen小 表示这个位置是可以到达的,i + numsi处,
  2. 当所处位置 i >= maxLen ,说明当前的脚下这步根本走不到(被中间的 0 卡死了,无法跳跃),直接返回 false

贪心选择 :我们每次都更新 maxReach = Math.max(maxReach, i + nums[i]),始终保持手里的视野是最远的。

如果能把n遍历完,代表可以走完楼梯,返回true

2. 代码

java 复制代码
class Solution {

    public boolean canJump(int[] nums) {

        int n = nums.length;

        int []dp = new int [n+1];

        int k = 0;//记录能从起点跳跃到的最远距离

        for(int i = 0;i < n;i++){

            //i > k说明永远无法跳跃到末尾,maxReach 是单调不减的后续i只会大于k,不可达

            if(i > k) return false;

            k = Math.max(k,i + nums[i]);

        }

        return true;

    }

}

二,跳跃游戏(二)

附上题目链接

https://leetcode.cn/problems/jump-game-ii/description/

1.思路

本题和上一题的不同点在于是求出到达楼梯顶的最小跳跃次数

对于我们的每次跳跃操作,都会到达一个最终点, 在这个最终点范围内的任何楼梯都能作为起跳点,我们的目的就是在这个范围内找出一个起跳点,让我们能够从这个位置跳跃到达的距离比上一步的最远边界更大

在范围内这个起跳点的存在是未知的,可能永远也无法超出我们之前能够到达的最远距离,所以还需要使用贪心来维护一个变量end作为可达最远距离 , maxReach则代表在end范围内寻找一个能跳的更远的跳板

这就需要我们使用贪心始终更新maxReach

2.代码(贪心)

java 复制代码
class Solution {
    public int jump(int[] nums) {
        int jumps = 0;          // 跳跃次数
        int end = 0;            // 当前跳跃步数能达到的最远边界
        int nextMaxReach = 0;   // 在当前边界内,下一跳能达到的最远边界
        
        // 注意:i < nums.length - 1,不需要遍历最后一个终点
        for (int i = 0; i < nums.length - 1; i++) {
            // 贪心:在当前能走的范围内,找出能跳得最远的下一个"跳板"
            nextMaxReach = Math.max(nextMaxReach, i + nums[i]);
            
            // 走到了当前这一步的边界,必须强制"跳"一下,并切换到新的边界
            if (i == end) {
                jumps++;
                end = nextMaxReach; // 这一步跳完,最远能到新的边界
            }
        }
        return jumps;
    }
}

除此之外对于该题还有一种思路则是使用动态规划, 毕竟是我最初想到的解法,虽然时间复杂度为O(N^2)

我们可以定义一个dp表, dpi则表示到达i位置所需要的最小跳跃次数

初始状态, dp0 = 0 ,无需跳跃操作

当我们到达i位置,此时跳跃范围就变为 i , i + nums\[i ] ,我们可以从最远距离i + numsi (姑且称为end吧) 从end向前寻找一个跳板,这个跳板有两个要求

  1. 可以到达,或者超越end
  2. 必须距离end越远越好,这代表需要跳跃的次数会更少

所以我们就可以来在向前寻找最远跳板的过程中来维护dp表, 注意这里dpi表示:到达i位置所需要的最小跳跃次数

在最初我们把dp表中除了0 位置的所有值初始化为一个非常大的数,表示初始不可到达

代码(动态规划)

java 复制代码
class Solution {

    public int jump(int[] nums) {

        int n = nums.length;

        int []dp = new int[n];

        //dp[i]:到达i位置所需要的最小跳跃次数

        Arrays.fill(dp,Integer.MAX_VALUE);

        dp[0] = 0;//起点不用跳

        for(int i = 1;i < n;i++){

            //向前推导起跳位置,找最小的起跳点

            for(int j = 0;j < i;j++){

                if(j + nums[j] >= i){//j位置可以通过一次跳跃到达i位置,或者比i更远的位置

                    //更新最小值

                    dp[i] = Math.min(dp[j] + 1,dp[i]);

                }

            }

        }

        return dp[n-1];

    }

}

虽然上述 DP 代码能在 LeetCode 上通过,但 O(n2)O(n^2)O(n2) 的时间复杂度在数据量极大时效率较低。如果我们仔细观察这个 DP 数组,会发现一个非常神奇的数学性质:dpdpdp 数组是单调非递减的

nums = [2, 3, 1, 1, 4] 为例:

  • dp0=0dp0 = 0dp0=0

  • 从索引 0 最远到索引 2,所以 dp1,dp2dp1, dp2dp1,dp2 都可以由 dp0+1dp0 + 1dp0+1 得到,即 [0, 1, 1]

  • 接下来从索引 1 扩散,最远到索引 4,所以 dp3,dp4dp3, dp4dp3,dp4 都可以由 dp1+1dp1 + 1dp1+1 得到,即 [0, 1, 1, 2, 2]

会发现,dpdpdp 数组的值变成了成块的:[0, 1, 1, 2, 2]

既然它是单调的,当我们在内层循环中从左往右找第一个能到达 iii 的 jjj 时,第一个找到的 jjj 对应的 dpjdpjdpj 一定是最小的

所以我们根本不需要内层循环去遍历所有的 jjj,只需要用一个指针固定在左边,随着 iii 的右移而缓缓右移。这进一步将时间优化到了 O(n)O(n)O(n),其本质逻辑就与维护"当前步数边界"的贪心算法不谋而合了。

这种用 DP 先推导出正确性,再通过"单调性"优化掉一层循环的思路,在解决各种高阶算法题时是非常强大的通用技巧。

相关推荐
Jack206 小时前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法
小小杨树7 小时前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
JieE2121 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2121 天前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
vivo互联网技术1 天前
CVPR 2026 | 全新强化学习框架 BeautyGRPO:重塑真实人像
算法·大模型·cvpr·影像
Darling噜啦啦1 天前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试
用户497863050731 天前
(一)小红的数组操作
算法·编程语言
怕浪猫1 天前
Electron 系列文章封面图
算法·架构·前端框架
徐小夕2 天前
JitWord 3.0 正式发布,高精度Word异构解析+复杂组件兼容,打造web端协同Word编辑器
前端·vue.js·算法