题目:
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
方法一: 回溯法
有一组数据超时。
- 遍历每个元素,对于每个元素i,先跳最大的步数nums[i],到达下一个元素next。
- 如果能够跳出这个数组(nums[i]+i>=nums.length-1),那么它一定可以跳到这个数组的最后一个元素。如果不能够跳出这个数组:
(1)next的值如果为0,则这条路是不可行的,则进行回溯,注意:是进行回溯,不能直接返回false。
(2)next的值如果不为0,则从该结点开始进行递归
代码实现如下:
public boolean canJump(int[] nums, int start) {
// 基本情况,反复递归,如果跳到最后一个元素
if (start == nums.length - 1)
return true;
for (int i = nums[start]; i > 0; i--) {
if (i + start >= nums.length - 1)
return true;
if (nums[start + i] != 0) {
//这里是回溯的处理方式!
if (canJump(nums, start + i))
return true;
}
}
return false;
}
当我们对方法一进行递归树的分析,
输入: [3,2,1,0,4]
元素 | 3 | 2 | 1 | 0 | 4 |
---|---|---|---|---|---|
角标 | 0 | 1 | 2 | 3 | 4 |
注c(i)==canJump(nums,i)
我们可以看出递归树有很多重复的子问题,因此我们对它进行剪枝,因为 C(3)已经被计算过不能走了,因此我们将C(2)中的C(3)剪去,而C(2)不能到达C(0),因此C(2)也不能走到。C(1)中的C(2)和C(3)之前都已经证明不能走,因此没有必要再进行计算。
根据上述优化,我们引入第二种方法。
方法二:动态规划记忆法(自上而下)
利用一个Demo数组,数组元素初始化为0,对所有情况进行标记,如果能够到达最后一个元素,则标记为1,不能到达标记为-1。
代码修改:在return前要先赋值,在遍历前要先看在记忆里有没有访问过
public boolean canJump(int[] nums) {
int[] demo = new int[nums.length];
for (int i = 0; i < nums.length; i++)
demo[i] = 0;
return helper(nums, 0, demo);
}
public boolean helper(int[] nums, int start, int[] demo) {
if (start == nums.length - 1) {
demo[start] = 1;//在return前要先赋值,
return true;
}
//在遍历前要先看在记忆里有没有访问过
if (demo[start] == 0) {
for (int i = nums[start]; i > 0; i--) {
if (i + start >= nums.length - 1) {
demo[start] = 1;//在return前要先赋值,
return true;
}
if (nums[start + i] != 0) {
if (helper(nums, start + i, demo)) {
demo[start] = 1;//在return前要先赋值,
return true;
}
}
}
demo[start] = -1;//在return前要先赋值,
return false;
}
return demo[start] == 1;
}
方法三:动态规划制表法(自下而上)
记忆法(自上而下)是从左往右遍历元素看是否是能够到达最后一个元素。那么表格法(自下而上)就是从右边到左边去遍历。
- 最右边一个肯定是,自身能够到达自身(can)
- 左边的是不是,取决于它能不能跳到它右边的can,也就是说在它能跳的范围之内是不是有can
- 如果它能够跳到can,则说明它也是can,否则,它是can’t
- 依次往左,看最左边一个是不是can
public boolean canJump(int[] nums) {
int[] table = new int[nums.length];
table[nums.length - 1] = 1;
for (int i = nums.length - 2; i >= 0; i--) {
int fur = Math.min(i + nums[i], nums.length - 1);
for (int j = i + 1; j <= fur; j++) {
if (table[j] == 1)
table[i] = 1;
}
}
return table[0] == 1;
}
我们可以将上述问题再进行优化,本来看一个元素是不是can,是遍历它右边的每个元素看有没有can,现在我们不用去遍历它右边的每个元素,而是利用一个变量closed
跟踪它右边离它最近的can。于是我们引入贪心算法。
方法四:贪心算法
这个解法是贪心算法的原因是它右边的元素已经处理完,离它最近的can也已经被我们用closed
跟踪了,因此,我们不用再去考虑我们已经解决的右边元素。而动态规划还要继续遍历右边的元素以寻找can。这就是贪心算法和动态规划最主要的区别。贪心算法不用再去考虑已经解决的子问题。关于贪心算法
public boolean canJump(int[] nums) {
int[] table = new int[nums.length];
table[nums.length - 1] = 1;
int closed = nums.length - 1;//记录右边can元素
for (int i = nums.length - 1; i >= 0; i--) {
if (i + nums[i] >= closed) {
table[i] = 1;
closed = i;//跟踪最左边的can元素
}
}
return table[0] == 1;
}
我们可以将算法再进行优化,因为closed
是用来标记最左边的can,因此,只要closed
是0,则第0个元素是最左边的can。因此,我们有方法五。
方法五:贪心算法优化
public boolean canJump(int[] nums)
{
int closed=nums.length-1;
for (int i=nums.length-2;i>=0 ;i-- ) {
if(i+nums[i]>=closed)
closed=i;
}
return closed==0;
}