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

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

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

一, 跳跃游戏

附上题目链接

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

相关推荐
KaMeidebaby1 小时前
卡梅德生物技术快报|酵母双杂交 cDNA 文库构建与蛋白互作筛选流程
服务器·前端·数据库·人工智能·算法
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2026.05.27 题目:3121. 统计特殊字母的数量 II
笔记·算法·leetcode
ST——Jess2 小时前
年度行业趋势研究报告:泛心理数字化赛道“流日推演”的算法困境与高保真交互范式重构
人工智能·算法·架构
Tisfy2 小时前
LeetCode 3300.替换为数位和以后的最小元素:一次遍历
数学·算法·leetcode·模拟
garmin Chen2 小时前
LeetcodeHot100打卡(14、合并空间,15、轮转数组,16、除了自身以外数组乘积,17.缺失的第一个整数)
java·笔记·学习·算法
elseif1232 小时前
【C++】vector 详细版
开发语言·c++·算法
变量未定义~2 小时前
既约分数、阶乘约数、逆元、最大质因子个数【算法赛】
算法
KaMeidebaby3 小时前
卡梅德生物技术快报|Western Blot 实验应用:肺肠轴机制研究全流程技术解析
前端·数据库·人工智能·算法·百度