代码随想录刷题day32|买卖股票的最佳时机II&跳跃游戏&跳跃游戏II

文章详细解析了买卖股票中寻找最大利润的贪婪算法,以及如何通过理解覆盖范围和贪心策略解决跳跃游戏问题,包括代码示例和逻辑分析。
摘要由CSDN通过智能技术生成


day32学习内容

day32主要内容

  • 买卖股票的最佳时机II
  • 跳跃游戏
  • 跳跃游戏II

声明
本文思路和文字,引用自《代码随想录》

一、买卖股票的最佳时机II

122.原题链接

1.1、思路

假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。

相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
也就是prices[3] - prices[0] = 每天的利润之和。

只收集每天的正利润,收集正利润的区间,就是股票买卖的区间,而我们只需要关注最终利润,不需要记录区间。

此题贪心在哪里?只收集正利润就是贪心所贪的地方!

局部最优:收集每天的正利润,全局最优:求得最大利润。

1.2、代码-正确写法

class Solution {
    public int maxProfit(int[] prices) {
        int result = 0;
        for (int i = 1; i < prices.length; i++) {
            // 只取正利润,和0取最大值,是负数直接加0,是正数就加prices[i] - prices[i - 1]
            result += Math.max(prices[i] - prices[i - 1], 0);
        }
        return result;
    }
}

1.2.1、如何理解result += Math.max(prices[i] - prices[i - 1], 0)

算法逻辑
  • int result = 0;: 初始化变量result用于累积总利润,初始值设为0。
  • for (int i = 1; i < prices.length; i++) { ... }: 通过一个循环遍历prices数组(从第二个元素开始,即i = 1),比较连续两天的价格,以决定是否进行交易。
    • result += Math.max(prices[i] - prices[i - 1], 0);: 这行代码是算法的核心。它计算连续两天的价格差(prices[i] - prices[i - 1]),如果价格差是正的,表示第i-1天买入、第i天卖出可以获得利润,所以将这个正的价格差累加到result中;如果价格差是负的或零,表示没有利润(或亏损),则不进行操作(通过Math.max(…, 0)实现,即如果价格差小于0,则加0,否则加价格差)。

二、跳跃游戏

55.原题链接

2.1、思路

此段文字摘抄字代码随想录
https://www.programmercarl.com/0055.%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8F.html#%E6%80%9D%E8%B7%AF

刚看到本题一开始可能想:当前位置元素如果是 3,究竟是跳一步呢还是两步呢还是三步呢,究竟跳几步才是最优呢?
其实跳几步无所谓,关键在于可跳的覆盖范围!
不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。
这个范围内,不用管是怎么跳的,反正一定可以跳过来。
那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

2.2、正确写法1

class Solution {
    public boolean canJump(int[] nums) {
        if (nums.length == 1) {
            return true;
        }
        // 初始化覆盖范围,为什么为0?因为开始没有调,所以是0
        int coverRange = 0;
        // 在覆盖范围内更新coverRange
        for (int i = 0; i <= coverRange; i++) {
            // 当前为位置i,最大可达位置为i+nums[i]
            coverRange = Math.max(coverRange, i + nums[i]);
            if (coverRange >= nums.length - 1) {
                return true;
            }
        }
        return false;
    }
}

2.2.1、 如何理解上面这段代码

逻辑详解
  1. 特殊情况处理:

    • if (nums.length == 1) { return true; }: 如果数组长度为1,意味着我们已经在最后一个位置上,不需要任何跳跃就已经达成条件,因此返回true
  2. 初始化覆盖范围:

    • int coverRange = 0;: 初始化变量coverRange,表示当前能覆盖到的最远距离。初始值为0,因为我们还没开始跳跃。
  3. 循环更新覆盖范围:

    • for (int i = 0; i <= coverRange; i++) {...}: 遍历数组nums,但只在当前覆盖范围内进行遍历。这是因为,如果某个位置超出了当前的覆盖范围,那么这个位置就无法被到达。
    • coverRange = Math.max(coverRange, i + nums[i]);: 在每一步,更新coverRange。这里i + nums[i]计算的是如果从位置i进行跳跃,最远能到达的位置。Math.max确保coverRange总是存储当前能到达的最远位置。
    • if (coverRange >= nums.length - 1) { return true; }: 如果在任何时刻coverRange超过或等于数组的最后一个位置的索引,那么说明可以到达数组的最后一个位置,方法返回true
  4. 无法到达最后位置:

    • 如果循环结束,意味着即使尽可能地跳跃,也无法覆盖到数组的最后一个位置,此时方法返回false

2.2.2、为什么要i + nums[i]?

在这个问题中,i + nums[i]的计算是为了确定从当前位置i开始,最远能跳到哪里。这里的逻辑是基于题目的规则:你在位置i时,可以跳跃的最大长度是nums[i]。因此,如果你现在处于位置i,那么你最远可以跳到i + nums[i]这个位置。

解释
  • i(当前位置): 表示你现在所在数组的位置。
  • nums[i](跳跃能力): 数组nums中的每个元素表示从那个位置最多可以向前跳跃的步数。因此,nums[i]表示从位置i最多可以跳跃的步数。
  • i + nums[i](最远可达位置): 当你在位置i时,向前跳nums[i]步,你将会到达的位置。这个计算显示了从当前位置出发,依据你的最大跳跃能力,能够到达的最远位置。
为什么重要

在解决“跳跃游戏”问题时,我们需要不断地更新在数组中能够到达的最远位置。通过比较coverRange(当前能覆盖到的最远距离)和i + nums[i](从当前位置i出发能到达的最远距离),我们可以确保coverRange始终保持在最大值。这是因为,在实际操作中,我们可能不需要每次都跳到最大距离,但了解能跳到的最远距离对于判断是否能到达数组末尾至关重要。

这种方式允许我们在遍历数组的过程中,用最小的步数高效地更新能够到达的最远位置,从而判断是否能够到达数组的最后位置。如果在某一步coverRange已经大于等于数组的最后一个索引,那么就意味着我们可以到达数组的最后位置。这是一个运用贪心算法思想的典型例子,通过局部最优选择(每一步尽可能跳得远),来达到全局的最优解(到达数组末尾)。

2.2.3、还是看不懂?举个具体例子帮助理解

假设有一个数组nums = [2, 3, 1, 1, 4],我们逐步分析代码的执行过程:

  1. 初始覆盖范围coverRange = 0,表示在开始时我们还没有开始移动,因此覆盖范围为0。
  2. 开始遍历数组,i = 0时,nums[0] = 2,这意味着从位置0,我们最多可以向前跳2步。因此,coverRange更新为max(0, 0 + 2) = 2。现在覆盖范围扩展到了索引2的位置。
  3. 下一步,i = 1nums[1] = 3,从位置1我们可以跳3步,这意味着我们可以达到更远的位置,即coverRange = max(2, 1 + 3) = 4。现在覆盖范围扩展到了数组的末尾。
  4. 由于覆盖范围已经达到或超过了数组的最后一个位置(覆盖范围索引从0开始,所以数组的最后一个位置是nums.length - 1 = 4),循环会提前终止,并返回true,表示我们可以跳到数组的最后位置。

三、跳跃游戏II

45.原题链接

3.1、思路

  • 翻译成人话就是计算跳跃到终点的最少步数。

3.2、正确写法1

class Solution {
    public int jump(int[] nums) {
        int result = 0;
        int end = 0;
        int temp = 0;
        // 在可以覆盖的范围内遍历,且只有当我们还没有到达数组末尾时才继续。
        for (int i = 0; i <= end && i < nums.length - 1; i++) {
            // 计算当前最大可覆盖范围
            temp = Math.max(i + nums[i], temp);
            // 遍历到了当前跳跃可以覆盖的最远距离下标,还没有到数组的终点位置,需要继续往下走1步。
            if (i == end) {
                end = temp;
                result++;
            }
        }
        return result;
    }
}

3.2.1、如何理解这段代码

理解这段代码的关键在于理解endtemp两个变量的作用:

  • end变量表示当前跳跃可以覆盖的最远距离下标。初始时,由于我们还没有开始跳跃,它被设置为0。
  • temp变量表示下一次跳跃可以覆盖的最远距离下标。它用于在遍历过程中不断更新我们可以达到的最远距离。

算法执行的流程如下:

  1. 初始化result为0,表示跳跃次数;end为0,表示当前覆盖的最远距离下标;temp为0,表示下一步覆盖的最远距离下标。
  2. 遍历数组元素(注意,循环的条件是i <= end && end < nums.length - 1,意味着我们会在可以覆盖的范围内遍历,且只有当我们还没有到达数组末尾时才继续)。
  3. 在每一次遍历中,我们更新tempi + nums[i]temp中的较大值,这表示如果从当前位置i跳跃,我们能到达的最远位置。
  4. 当遍历到当前覆盖的最远距离end时(即i == end),说明我们需要进行一次跳跃以到达更远的位置。这时,我们将end更新为temp(因为temp是我们在当前覆盖范围内可以达到的最远距离),并将跳跃次数result增加1。
  5. 重复步骤3和4,直到覆盖范围包含数组的最后一个位置,这时跳出循环。
  6. 返回result作为结果,即到达数组末尾所需的最少跳跃次数。

通过这种方式,代码贪心地在每一步跳跃中都尽可能向前跳到最远的位置,从而确保跳跃次数最少。

3.2.2 、如何理解i == end

在这段代码中,i == end这个条件用于判断是否遍历到了当前跳跃可以覆盖的最远距离下标。这里的end变量表示在当前跳跃次数下,我们可以到达的最远位置的索引。每当i(当前遍历到的位置的索引)与end相等时,意味着我们已经到达了当前跳跃能够覆盖的最远范围的边界。此时有两个关键点需要注意:

  1. 跳跃次数的增加:当i达到了当前的end时,我们需要进行一次新的跳跃(即增加跳跃次数result),因为我们已经尽可能地利用了当前跳跃的能力,而要进一步前进,就必须开始一次新的跳跃。

  2. 更新下一次的最远距离:在每次遍历过程中,我们通过比较tempi + nums[i]来更新下一次跳跃可以到达的最远距离。而当i == end时,即我们达到了当前能达到的最远距离,这时我们就需要把end更新为temp,因为temp此时代表了下一次跳跃可以到达的最远距离。

举个具体例子:

假设数组是 nums = [2, 3, 1, 1, 4]

初始时,我们在位置0,end = 0,表示当前跳跃能达到的最远距离(这里是指下标,下标从0开始),temp = 0表示下一步跳跃可以到达的最远距离,result = 0表示跳跃次数。

  1. 第一轮循环: 初始i = 0end = 0

    • i = 0时,nums[0] = 2,表示从位置0出发最多可以向前跳2步。所以,temp = max(temp, i + nums[i]) = max(0, 0 + 2) = 2。这意味着在当前这一跳中,我们可以到达的最远是下标为2的位置。
    • 在这轮循环的结束时(i == end),意味着我们已经达到了当前跳跃能到达的最远位置。此时我们将end更新为temp的值(也就是2),表示下一次跳跃的最远范围。同时,result++表示完成了一次跳跃。
  2. 第二轮循环: 现在,i会从1继续增加,直到i = end(此时end = 2)。

    • i = 1时,nums[1] = 3,这意味着从位置1出发最多可以向前跳3步,因此temp更新为max(2, 1 + 3) = 4。现在temp表示我们从当前所有检查的位置出发能到达的最远位置是下标4。
    • i = 2时,即i达到了当前end的值,这标志着需要进行一次新的跳跃。因为temp此时为4,表示下一次跳跃能达到的最远位置已经是数组的最后了。我们将end更新为temp,同时result++

通过上面的过程,我们完成了两次跳跃就到达了数组的最后一个位置,因此函数返回result = 2

这个例子中,“当i增加到end的值时,说明我们已经达到了当前跳跃能够到达的最远位置”这句话的含义是:在每次的跳跃过程中,当我们遍历到end所标记的位置时,意味着我们已经利用当前这一跳的能力达到了最大范围,此时需要基于temp(记录了从起点到当前点的所有可能的最远跳跃点)的值来开始下一跳,并且这一跳的结束点(即下一次的end)就是当前的temp

总结

1.感想

  • 跳跃游戏II这鬼题目都看不懂,还是看卡尔的翻译一下才看得懂题目,题意不说人话的,翻译成人话就是计算跳跃到终点的最少步数。
  • 跳跃游戏II写不出来,抄的题解。。

2.思维导图

本文思路引用自代码随想录,感谢代码随想录作者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值