问题:
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
说明:
假设你总是可以到达数组的最后一个位置。
输入输出:
class Solution {
public int jump(int[] nums) {
}
}
方案设计:
采用递归的方式,寻找从第i个数开始,到达最后所需的最少的步数,很简单,直接贴代码。
class Solution {
public int jump(int[] nums) {
return jum(nums,0);
}
public int jum(int[] nums,int i){
if(i==nums.length-1){
return 0;
}
if(i+nums[i]>=nums.length-1){
return 1;
}
int value=nums.length;//步数最大也就是n个数,走n步
for(int j=1;j<=nums[i];j++){
if(i+j<nums.length-1&&nums[i+j]>0){
value=Math.min(value,1+jum(nums,i+j));
}
}
return value;
}
}
提交的结果是:71 / 92 个通过测试用例。在下面这个测试用例里报出超时:
[5,6,4,4,6,9,4,4,7,4,4,8,2,6,8,1,5,9,6,5,2,7,9,7,9,6,9,4,1,6,8,8,4,4,2,0,3,8,5]
很明显,递归的写法太简单,时间复杂度太高,才到了71个用例就卡死了,不行,要优化。这里开始采用递归向动态规划转化的思想。学习了《算法导论》里的动态规划的一章,以及要特别感谢一下这篇博客:
算法-动态规划 Dynamic Programming–从菜鸟到老鸟。
优化一:
一般来说,递归耗时太久是因为重复的计算了太多的单元,比如一共有200个数字要计算,其中第188个数字(计算从它开始至少要几次才能跳到最后),可能要计算几十次,这样肯定是不科学的,所以,递归向动态规划转换的第一步,可以先考虑下备忘录的思想,用空间换时间,将已经计算过的结果保存一下,下一次直接用就可以了。代码如下:
class Solution {
public int jump(int[] nums) {
int[] v=new int[nums.length];//用它保存从第i位开始,往后至少要几步
for(int i=0;i<v.length;i++){
v[i]=-1;
}
return jum(nums,0,v);
}
public int jum(int[] nums,int i,int[] v){
if(v[i]>-1){//如果已经有值,则直接返回
return v[i];
}
if(i==nums.length-1){
v[i]=0;
return 0;
}
if(i+nums[i]>=nums.length-1){
v[i]=1;
return 1;
}
int value=Integer.MAX_VALUE-1;
for(int j=1;j<=nums[i];j++){
if(i+j<nums.length-1&&nums[i+j]>0){
value=Math.min(value,1+jum(nums,i+j,v));
}
}
v[i]=value;
return value;
}
}
运行的结果是:91 / 92 个通过测试用例,只有最后一个报了超时,最后一个的测试用例非常长,有兴趣的可以点进去看一下,测试用例。这个测试用例我单独运行了一下,直接就报了超时,时间都没法统计。所以,说明备忘录的方式是有效果的,但是还远远不够,所以还要接着优化,那么就进行动态规划,自底向上的编程思想。
优化二:
之前的设计是,寻找从第i位开始向后至少需要几位,这种思想是递归的思想是自顶向下的,用于动态规划自底向上就不是很适合。所以要转换下思想,设一个数组v[],v[i]保存的不再是从第i位开始,往后至少需要几步,而是保存到达i时,至少需要几步,这样的话,i从0开始往后递推,就能得到最后的结果,而且是每次递推,都依赖上一次的结果,正好是自底向上的思想。言语说可能有点不太好理解,画图说明:
把v数组的第0位设为0很容易理解,其后的我设为了5,因为数据一共5个,最大的步数也不会超过5,也比较容易理解。然后依次处理各个数据,处理的方式是,首先得到nums[i]的值,它代表了能往后跳多远,而v[i]代表了从开始到第i位,至少要跳多少步,所以,要刷新v[j],i+1<=j<=i+nums[i],判断如果是v[i]+1(从当前位往后跳一次)比v[i+j]小的话,就令v[i+j]=v[i]+1,这样就得到了v[i+j]的值(到达第i+j至少需要几步)。比如处理第一个数据的时候,过程如图:
然后依次是:
最后就得到了结果。
代码如下:
class Solution {
public int jump(int[] nums) {
int length=nums.length;
int[] v=new int[length];//定义v表示到达数组的当前位置至少需要几步
for(int i=1;i<length;i++){
v[i]=length;
}
for(int i=0;i<length;i++){//第一个不用考虑,肯定是0
for(int j=1;j<=nums[i]&&i+j<length;j++){
if(v[i]+1<v[i+j]){
v[i+j]=v[i]+1;
}
}
}
return v[nums.length-1];
}
}
执行这个代码,结果还是超时,依旧是最后一个用例没有通过,我单独运行了一下这个用例,发现时间已经有了,是317ms。
说明思路应该是对的,就是里面有些关键的东西被我忽略了,期间我注意到了,其实当第一次到达结尾的时候,就已经得到最终答案了,所以我尝试着进行了这样的改进:
class Solution {
public int jump(int[] nums) {
int length=nums.length;
int[] v=new int[length];//定义v表示到达数组的当前位置至少需要几步
if(length==1){
return 0;
}
for(int i=1;i<length;i++){
v[i]=length;
}
for(int i=0;i<length;i++){//第一个不用考虑,肯定是0
if(i+nums[i]>=length-1){//第一次到达则立刻返回
return v[i]+1;
}
for(int j=1;j<=nums[i]&&i+j<length;j++){
if(v[i]+1<v[i+j]){
v[i+j]=v[i]+1;
}
}
}
return v[nums.length-1];
}
}
最终的结果是,只提高了2ms,还是超时,说明仍需要优化。
优化三
在优化二的基础之上,i的递增是每次+1,其实,是没有必要这样的,比如对于数组[4,2,2,2,5,6],从4直接跳到5即可,中间的2的计算,是没有任何意义的,那么i每次+1就要优化成这样:i每次要走最佳一大步,这一大步可以定义成一步能覆盖的范围,范围越大,这一步越优,具体讲,就是从当前位置开始,往后走一步加上走一步后的那个位置最大能覆盖到的举例。比如4,它可以走到,2,2往后能覆盖2位,也就是它最多推进3位,同理,后面的2分别推进4,5位,而5,则能推进9位,也就是说下一步从5开始往后推进就行,之前的不可能比它更优。
代码如下:
class Solution {
public int jump(int[] nums) {
int length=nums.length;
int[] v=new int[length];//定义v表示到达数组的当前位置至少需要几步
if(length==1){
return 0;
}
for(int i=1;i<length;i++){
v[i]=length;
}
int i=0;
int max=0;
int ss=0;
while(i<length){//第一个不用考虑,肯定是0
if(i+nums[i]>=length-1){
return v[i]+1;
}
max=nums[i+1]+1;
ss=1;
for(int j=1;j<=nums[i]&&i+j<length;j++){
if(nums[i+j]+j>max){
max=nums[i+j]+j;
ss=j;
}
if(v[i]+1<v[i+j]){
v[i+j]=v[i]+1;
}
}
i=i+ss;
}
return v[nums.length-1];
}
}
提交结果是12ms,终于通过了~但是只超越了40%的人。而作为一个铁头娃,我依然不满足。
就看了一下评论区,发现还可以再优化。
优化四
其实就这个问题来说,v[i]其实也可以不需要,因为推进的时候,已经将推进优化成了每次寻找宽度最大的下一个数字,那么定义一个step,每寻找一次,step++,最后到达末尾的时候,直接输出不就得了嘛~
代码如下:
class Solution {
public int jump(int[] nums) {
int length=nums.length;
if(length==1){
return 0;
}
int i=0;
int max=0;
int ss=0;
int step=0;
while(i<length){//第一个不用考虑,肯定是0
if(i+nums[i]>=length-1){
return ++step;
}
max=nums[i+1]+1;
ss=1;
for(int j=1;j<=nums[i]&&i+j<length;j++){
if(nums[i+j]+j>max){
max=nums[i+j]+j;
ss=j;
}
}
i=i+ss;//优化了一下递进的速度
step++;
}
return step;
}
}
运行结果是9ms或者10ms,超越72%。作为一个铁头娃,不超越90%我是不会满足的。于是我又看了一下评论区。
优化五
直接贴那个老哥的代码:
class Solution {
public int jump(int[] nums) {
if(nums.length == 1) return 0;
int reach = 0;
int nextreach = nums[0];//第一步能走多远,初始化
int step = 0;
for(int i = 0;i<nums.length;i++){
nextreach = Math.max(i+nums[i],nextreach);//一步能走多远
if(nextreach >= nums.length-1) return (step+1);
if(i == reach){//记步,
step++;
reach = nextreach;
}
}
return step;
}
}
实际上思想是一样的,就是换了种写法,少了点儿步骤,但是这种写法在一些需要多次循环的运算里确实是有很大作用的,所以程序的优化,思想是第一步,然后程序的精简和科学性是第二步,当然,这个版本的可读性是最差的,因为就像高数老师给讲题一样,不说前面直接看这个,基本上是不可能看懂的。运行一下,结果是7ms,超越92%。
一本满足。最后再看下程序,发现一个问题,这特么不是贪心算法吗。。。
看来还是需要继续培养自己的解决问题的直觉呀。。。