给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例 1:

输入: [2,3,1,1,4]
输出: true
解释: 从位置 0 到 1 跳 1 步, 然后跳 3 步到达最后一个位置。
示例 2:

输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/jump-game

此题很经典,可用回溯、动态规划、贪心求解并对比,我看题第一反应用回溯结果超时

我的代码(超时):

public class Solution55 {
boolean res = false;
public boolean canJump(int[] nums) {
canJump(nums,0);
return res;
}
public void canJump(int[] nums,int begin) {
if (nums[begin] >= nums.length-1-begin) {
res = true;
}
for (int i=begin+1;i<=Math.min(nums.length-1,begin + nums[begin]);i++) {
canJump(nums,i);
if (res == true) {
break;
}
}
}
}

下面是leetcode官方题解,很详细!

定义
如果我们可以从数组中的某个位置跳到最后的位置,就称这个位置是“好坐标”,否则称为“坏坐标”。问题可以简化为第 0 个位置是不是“好坐标”。
题解
这是一个动态规划问题,通常解决并理解一个动态规划问题需要以下 4 个步骤:

1.利用递归回溯解决问题
2.利用记忆表优化(自顶向下的动态规划)
3.移除递归的部分(自底向上的动态规划)
4.使用技巧减少时间和空间复杂度
下面的所有解法都是正确的,但在时间和空间复杂度上有区别。

实际上leetcode的测试用例方法一回溯法和方法二自顶向下的动态规划都超时,只有方法三自底向上的动态规划和方法四贪心可AC

方法 1:回溯

(超时)
这是一个低效的解决方法。我们模拟从第一个位置跳到最后位置的所有方案。从第一个位置开始,模拟所有可以跳到的位置,然后从当前位置重复上述操作,当没有办法继续跳的时候,就回溯。

public class Solution {
public boolean canJumpFromPosition(int position, int[] nums) {
if (position == nums.length - 1) {
return true;
} int furthestJump = Math.min(position + nums[position], nums.length - 1);
for (int nextPosition = position + 1; nextPosition <= furthestJump; nextPosition++) {
if (canJumpFromPosition(nextPosition, nums)) {
return true;
}
} return false;
} public boolean canJump(int[] nums) {
return canJumpFromPosition(0, nums);
}
}

一个快速的优化方法是我们可以从右到左的检查 nextposition ,理论上最坏的时间复杂度复杂度是一样的。但实际情况下,对于一些简单场景,这个代码可能跑得更快一些。直觉上,就是我们每次选择最大的步数去跳跃,这样就可以更快的到达终点。

// Old
for (int nextPosition = position + 1; nextPosition <= furthestJump; nextPosition++)
// New
for (int nextPosition = furthestJump; nextPosition > position; nextPosition--)

比方说,对于下面的例子,我们从下标 0 开始跳,第一次跳到 1,第二次跳到 6。这样用 3 步就发现坐标 0 是一个“好坐标”。

下面的例子解释了上述优化没有办法解决的情况,坐标 6 是不能从任何地方跳到的,但是所有的方案组合都会被枚举尝试。

前几次回溯访问节点如下:0 -> 4 -> 5 -> 4 -> 0 -> 3 -> 5 -> 3 -> 4 -> 5 -> 等等。

复杂度分析

时间复杂度:O(2^n),最多有 2^n 种从第一个位置到最后一个位置的跳跃方式,其中 n 是数组 nums 的元素个数

空间复杂度:O(n),回溯法只需要栈的额外空间。

方法 2:自顶向下的动态规划

(实际上这个方法也超时)

自顶向下的动态规划可以理解成回溯法的一种优化。我们发现当一个坐标已经被确定为好 / 坏之后,结果就不会改变了,这意味着我们可以记录这个结果,每次不用重新计算。

因此,对于数组中的每个位置,我们记录当前坐标是好 / 坏,记录在数组 memo 中,定义元素取值为 GOOD ,BAD,UNKNOWN。这种方法被称为记忆化。

例如,对于输入数组 nums = [2, 4, 2, 1, 0, 2, 0] 的记忆表如下,G 代表 GOOD,B 代表 BAD。我们发现不能从下标 2,3,4 到达最终坐标 6,但可以从 0,1,5 和 6 到达最终坐标 6。

步骤

1.初始化 memo 的所有元素为 UNKNOWN,除了最后一个显然是 GOOD (自己一定可以跳到自己)
2.优化递归算法,每步回溯前先检查这个位置是否计算过(当前值为:GOOD / BAD)
  1.如果已知直接返回结果 True / False
  2.否则按照之前的回溯步骤计算
3.计算完毕后,将结果存入memo表中

enum Index {
GOOD, BAD, UNKNOWN
} public class Solution {
Index[] memo; public boolean canJumpFromPosition(int position, int[] nums) {
    //优化递归算法,每步回溯前先检查这个位置是否计算过(当前值为:GOOD / BAD
if (memo[position] != Index.UNKNOWN) {
return memo[position] == Index.GOOD ? true : false;
} int furthestJump = Math.min(position + nums[position], nums.length - 1);
for (int nextPosition = position + 1; nextPosition <= furthestJump; nextPosition++) {
if (canJumpFromPosition(nextPosition, nums)) {
memo[position] = Index.GOOD;
return true;
}
} memo[position] = Index.BAD;
return false;
} public boolean canJump(int[] nums) {
memo = new Index[nums.length];
for (int i = 0; i < memo.length; i++) {
memo[i] = Index.UNKNOWN;
}
memo[memo.length - 1] = Index.GOOD;
return canJumpFromPosition(0, nums);
}
}

 复杂度分析

时间复杂度:O(n^2),数组中的每个元素,假设为 i,需要搜索右边相邻的 nums[i] 个元素查找是否有 GOOD 的坐标。 nums[i] 最多为 nn 是 nums 数组的大小。

空间复杂度:O(2n)=O(n),第一个 n 是栈空间的开销,第二个 n 是记忆表的开销

方法 3:自底向上的动态规划

底向上和自顶向下动态规划的区别就是消除了回溯,在实际使用中,自底向下的方法有更好的时间效率因为我们不再需要栈空间,可以节省很多缓存开销。更重要的事,这可以让之后更有优化的空间。回溯通常是通过反转动态规划的步骤来实现的。

这是由于我们每次只会向右跳动,意味着如果我们从右边开始动态规划,每次查询右边节点的信息,都是已经计算过了的,不再需要额外的递归开销,因为我们每次在 memo 表中都可以找到结果。

enum Index {
GOOD, BAD, UNKNOWN
} public class Solution {
public boolean canJump(int[] nums) {
Index[] memo = new Index[nums.length];
for (int i = 0; i < memo.length; i++) {
memo[i] = Index.UNKNOWN;
}
memo[memo.length - 1] = Index.GOOD; for (int i = nums.length - 2; i >= 0; i--) {
int furthestJump = Math.min(i + nums[i], nums.length - 1);
for (int j = i + 1; j <= furthestJump; j++) {
if (memo[j] == Index.GOOD) {
memo[i] = Index.GOOD;
break;
}
}
} return memo[0] == Index.GOOD;
}
}

复杂度分析

时间复杂度:O(n^2),数组中的每个元素,假设为 i,需要搜索右边相邻的 nums[i] 个元素查找是否有 GOOD 的坐标。 nums[i] 最多为 nn 是 nums 数组的大小。

空间复杂度:O(n),记忆表的存储开销。

方法 4:贪心

当我们把代码改成自底向上的模式,我们会有一个重要的发现,从某个位置出发,我们只需要找到第一个标记为 GOOD 的坐标(由跳出循环的条件可得),也就是说找到最左边的那个坐标。如果我们用一个单独的变量来记录最左边的 GOOD 位置,我们就可以避免搜索整个数组,进而可以省略整个 memo 数组。

从右向左迭代,对于每个节点我们检查是否存在一步跳跃可以到达 GOOD 的位置(currPosition + nums[currPosition] >= leftmostGoodIndex)。如果可以到达,当前位置也标记为 GOOD ,同时,这个位置将成为新的最左边的 GOOD 位置,一直重复到数组的开头,如果第一个坐标标记为 GOOD 意味着可以从第一个位置跳到最后的位置。

模拟一下这个操作,对于输入数组 nums = [9, 4, 2, 1, 0, 2, 0],我们用 G 表示 GOOD,用 B 表示 BAD 和 U 表示 UNKNOWN。我们需要考虑所有从 0 出发的情况并判断坐标 0 是否是好坐标。由于坐标 1 是 GOOD,我们可以从 0 跳到 1 并且 1 最终可以跳到坐标 6,所以尽管 nums[0] 可以直接跳到最后的位置,我们只需要一种方案就可以知道结果。

public class Solution {
public boolean canJump(int[] nums) {
int lastPos = nums.length - 1;
for (int i = nums.length - 1; i >= 0; i--) {
if (i + nums[i] >= lastPos) {
lastPos = i;
}
}
return lastPos == 0;
}
}

复杂度分析

  • 时间复杂度:O(n)O(n),只需要访问 nums 数组一遍,共 nn 个位置,nn 是 nums 数组的长度。
  • 空间复杂度:O(1)O(1),不需要额外的空间开销。

总结
最后一个问题是,如何在面试场景中想到这个做法。我的建议是“酌情考虑”。最好的解法当然和别的解法相比更简单也更短,但是不那么容易直接想到。

递归回溯的版本最容易想到,所以在思考更复杂解法的时候可以顺带提及一下这个解法,你的面试官实际上可能会想要看到这个解法。但如果没有,请提及可以使用动态规划的解法,并试想一下如何用记忆表来实现。如果你发现面试官希望你回答自顶向下的方法,那么就不太需要思考自底向上的版本,但我推荐在面试中提及一下自底向下的优点。

很多人会在将自顶向下的动态规划转成自底向上版本时出现困难,多做一些相关的练习可以对你有所帮助。

【1】【经典回溯、动态规划、贪心】【leetcode-55】跳跃游戏的更多相关文章

  1. leetcode 55. 跳跃游戏 及 45. 跳跃游戏 II

    55. 跳跃游戏 问题描述 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 判断你是否能够到达最后一个位置. 示例 1: 输入: [2,3,1, ...

  2. Java实现 LeetCode 55 跳跃游戏

    55. 跳跃游戏 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 判断你是否能够到达最后一个位置. 示例 1: 输入: [2,3,1,1,4] ...

  3. 力扣Leetcode 55. 跳跃游戏

    跳跃游戏 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 判断你是否能够到达最后一个位置. 示例 1: 输入: [2,3,1,1,4] 输出: ...

  4. [LeetCode]55. 跳跃游戏(贪心)

    题目 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 判断你是否能够到达最后一个位置. 示例 1: 输入: [2,3,1,1,4] 输出: tr ...

  5. LeetCode 55. 跳跃游戏(Jump Game)

    题目描述 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 判断你是否能够到达最后一个位置. 示例 1: 输入: [2,3,1,1,4] 输出: ...

  6. leetcode 55. 跳跃游戏 JAVA

    题目: 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 判断你是否能够到达最后一个位置. 示例 1: 输入: [2,3,1,1,4] 输出: t ...

  7. 力扣Leetcode 45. 跳跃游戏 II - 贪心思想

    这题是 55.跳跃游戏的升级版 力扣Leetcode 55. 跳跃游戏 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 你的目标是使用最少的跳跃 ...

  8. LeetCode:跳跃游戏【55】

    LeetCode:跳跃游戏[55] 题目描述 给定一个非负整数数组,你最初位于数组的第一个位置.数组中的每个元素代表你在该位置可以跳跃的最大长度.判断你是否能够到达最后一个位置. 示例 1: 输入: ...

  9. LeetCode 45. 跳跃游戏 II | Python

    45. 跳跃游戏 II 题目来源:https://leetcode-cn.com/problems/jump-game-ii 题目 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素 ...

  10. [leetcode] 45. 跳跃游戏 II(Java)(动态规划)

    45. 跳跃游戏 II 动态规划 此题可以倒着想. 看示例: [2,3,1,1,4] 我们从后往前推,对于第4个数1,跳一次 对于第3个数1,显然只能跳到第4个数上,那么从第3个数开始跳到最后需要两次 ...

随机推荐

  1. c# MVC5(一) 初步认识以及新建mvc

    一:MVC5初始 1:广义MVC(Model--View-Controller): V是界面 : M是数据和逻辑 : C是控制,把M和V链接起来: 是程序设计模式,一种设计理念,可以有效的分离界面和业 ...

  2. Nginx 负载均衡实例redis

    Nginx 负载均衡实例redis 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.

  3. elasticsearch 索引和mapping导入导出命令

    导mapping:elasticdump \ --input=http://192.168.102.13:9200/search_v1 \ --output=http://192.168.102.69 ...

  4. django中运行定时任务脚本

    需要使用到django_apscheduler模块,因此先安装: pip install django-apscheduler 然后在工程的settings.py文件中的INSTALLED_APPS模 ...

  5. Flex弹性盒模型(新老版本完整)--移动端开发整理笔记(二)

    Flex布局 Flex即Flexible Box,写法为:display:flex(旧版:display: -webkit-box) 在Webkit内核下,需要加-webkit前缀: .box{ di ...

  6. DS18B20温度获取

    https://detail.tmall.com/item.htm?id=40083203373&spm=a1z09.2.0.0.31cd2e8d1sb06V&_u=e1qf7bf56 ...

  7. 使用plv8+hashids生成短链接服务

    有写过一个集成npm plv8 以及shortid生成短链接id服务,实际上我们可以集成触发器自动生成url对应的短链接地址,hashids也是一个不错的选择. 以下是一个别人写的一个博客实现可以参考 ...

  8. contest2 CF989 div2 ooox? ooox? oooo?

    题意 div2C (o) 在\(小于50*50\)的棋盘上放\(A, B, C, D\)四种花, 并给出每种花的连通块数量\(a, b, c, d(\le 100)\), 输出一种摆法 div2D ( ...

  9. django -- ORM建表

    前戏 ORM(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术. ORM的优势: ORM解决的主要问题是对象和关系的映射 ...

  10. 网络协议 9 - TCP协议(下)

    上次了解了 TCP 建立连接与断开连接的过程,我们发现,TCP 会通过各种“套路”来保证传输数据的安全.除此之外,我们还大概了解了 TCP 包头格式所对应解决的五个问题:顺序问题.丢包问题.连接维护. ...