题目链接:https://leetcode.com/problems/jump-game/
这是个十分有趣的题目。从起点(数组索引0)出发,移动k(k<=nums[0])个位置到达下一个位置(数组索引为i),继续移动k(k<=nums[i])个位置,就这样看是否有一条走法能到达终点(数组索引nums.length-1)。当走到某个位置i时nums[i]==0时就意味着这个走法行不通了!可以看到理论上每一个位置i上都有nums[i]个选择的步数。
我的思路:用回溯法来暴力搜索,在当前到达的位置状态i上,有1:nums[i]种选择(只要nums[i]+i<nums.length),然后就进入下一个(位置)状态继续搜索。
class Solution {
public static boolean canJump(int[] nums) {
return recursion(0,nums);
}
public static boolean recursion(int index,int[] nums)
{
if(index==nums.length-1)
return true;
if(nums[index]==0)
return false;
boolean ret=false;
for(int i=1;i<=nums[index];i++)
if(index+i<nums.length)
ret=ret || recursion(index+i,nums);
return ret;
}
}
时间复杂度:O(2^n),从起点到达终点有2^n的走法(理论上限)
空间复杂度:O(n),递归时的栈内存开销
暴力是真暴力,就是TLE了!!!
其实审查上面的过程,到达位置状态i的策略选择可以由很多很多种,而后面从i为起点的搜索只需要进行一次,然而我的算法存在大量重复。
看了Solutions后豁然开朗,https://leetcode.com/problems/jump-game/solution/这篇题解将回溯、动态规划讲的很全面很详细了,下面借鉴这篇题解写写自己的理解。
上面的步骤在实践中可以稍微优化一下,从较大调步开始搜索更快到达终点,但算法理论复杂度没变。
for(int i=nums[index];i>0;i--)
自顶向下的动态规划:
用一个memo数组存储位置i对于终点的可达性(accesibility),终点对于其本身是可达的,首先将memo数组memo[0:nums.length-2]=Unknown;
enum Mark{
Accessible,Inaccessible,Unknown
}
public class Solution {
public Mark[] memo;
public boolean canJump(int[] nums) {
memo=new Mark[nums.length];
for(int i=0;i<memo.length;i++)
memo[i]=Mark.Unknown;
memo[nums.length-1]=Mark.Accessible;
return canJumpFromIndex(0,nums);
}
public boolean canJumpFromIndex(int index,int[] nums){
if(memo[index]!=Mark.Unknown)
return memo[index]==Mark.Accessible? true:false;
int min_index=Math.min(nums[index]+index,nums.length-1);
for(int i=min_index;i>=index+1;i--)
{
if(canJumpFromIndex(i,nums))
{
memo[index]=Mark.Accessible;
return true;
}
}
memo[index]=Mark.Inaccessible;
return false;
}
}
时间复杂度为:O(n^2),因为对于每个状态位置i,都有可能将[i+1:nums.length-1]遍历一遍
空间复杂度:O(n),递归的栈空间开销O(n),额外数组开销O(n)
自底向上的动态规划:自底向上一般能够将递归改成循环的形式,而且能够减少中间重复的计算
确定第i个位置的可达性只需要检查第[i+1,i+min(nums[i]+i,nums.length-1)]的位置上的可达性,真是tricky!!!
public class Solution {
public Mark[] memo;
public boolean canJump(int[] nums) {
memo=new Mark[nums.length];
for(int i=0;i<memo.length;i++)
memo[i]=Mark.Unknown;
memo[nums.length-1]=Mark.Accessible;
for(int i=nums.length-2;i>=0;i--)
{
int max_index=Math.min(nums[i]+i,nums.length-1);
for(int j=max_index;j>i;j--)
{
if(memo[j]==Mark.Accessible)
{
memo[i]=Mark.Accessible;
break;
}
}
}
return memo[0]==Mark.Accessible?true:false;
}
}
enum Mark{
Accessible,Inaccessible,Unknown
}
最终极的方法的思路就是:要确定当前某个位置i的可达性,只需要找到大于i且具有可达性的最小的位置j,如果i到j可达(nums[i]+i>=j,这里如果nums[i]+i>nums.length-1就更能说明可以到达了)则i也可达,并将i设置为“具有可达性的最小的位置”,让我们一起欣赏一下这个代码:
public class Solution {
public boolean canJump(int[] nums) {
int minPosition=nums.length-1;
for(int i=nums.length-1;i>=0;i--)
{
if(nums[i]+i>=minPosition)
minPosition=i;
}
return minPosition==0;
}
}