55. Jump Game(跳跃游戏)
1. 题目描述
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3步到达最后一个位置。
示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 ,所以你永远不可能到达最后一个位置。
2. 回溯法(Backtracking, Time Limit Exceeded)
2.1 解题思路
对于这道题,回朔法是非常直接的思路:
- 从序号(index) = 0开始,取nums[index]的最大距离,跳到下一个index + nums[index]序号位置;
- 如果下一个nums[index]大于0,则继续1. 的步骤,直到抵到右边界的序号,返回true;
- 反之,如果下一个nums[index]为0,则返回上一个序号位置,取第二大的距离,跳到下一个序号,继续1. 的步骤,直到回到初始序号,此时表示不可能抵达右边界的序号,返回false;
下面的流程图直观地表示了上面的步骤:
当然1. 可以先取最小值,再取最大值,但是这样会稍微慢一些,我们可以优先取最大值,保证每次的移动的距离最大。
可惜的是,这个方法对于某些特殊的test case是会超时的,一般都是右边有0,比如下面两:1) test case1;2)test case2
2.2 实例代码
class Solution {
bool backTracking(vector<int>& nums, int i) {
if (i + nums[i] >= static_cast<int>(nums.size()) - 1) return true; // static_cast<int> - 因为需要用unsigned - 1,转换成int比较保险,否则0 - 1会溢出
// if (!nums[i]) return false; // 添加这句可以通过test case1,但是另一个不行
for (int j = nums[i]; j > 0; j--) if (backTracking(nums, j + i)) return true;
return false;
}
public:
bool canJump(vector<int>& nums) { return backTracking(nums, 0); }
};
3. 动态规划(Dynamic Programming)
3.1 解题思路
动态规划有两种思路:1)使用动态规划的回朔法;2)动态规划;
首先,我们先来看看2. 回溯法里面的第二个例子,我们发现1(序号2)无论如何都会走到0(序号3),2(序号1)也是最终都会走到0,无论是直接走两步来到0,还是先走到1,再走到0。所以我们知道这两个序号是不可能抵到终点,所以3(序号1)取到这两个序号的值一定是无效的,所以如果在之前回溯的过程中,我们记录下那些不可能抵达终点的序号和可以抵达终点的序号,分别标记为:GOON,NOWAY,如果来到这些有标记的点,GOON直接返回true,NOWAY直接返回false,这样就省去了一些递归过程。
对于没有标记的点,我们可以统一标记为UNKNOWN,正常进行2. 的回溯即可。具体的流程图如下:
可惜C++版本的这个思路还是超时~
但是走到这里,我们观察到一个重要的线索:如果我们能从后面开始动态规划,从后面记录下每个点是否能抵达终点。所以根据这个思路,我们可以写出动态规划的方程:
dp[i] = dp[i + k], 1 < k <= nums[i]
我们以下面这个例子来进行说明:
[0, 1, 2, 3, 4, 5, 6]
[2, 4, 2, 1, 0, 2, 0]
dp[5]能否达到6,取决于dp[5 + 1], dp[5 + 2](越界忽略),dp[6]默认能达到终点,所以dp[5] = GOON;以此类推,最后我们只需检查dp[0]是否等于GOON即可。具体的流程见下图:
3.2 实例代码
typedef enum explores { GOON, NOWAY, UNKNOWN } explores;
class Solution {
// 下面两个方法二选一即可
// 1.使用动态规划的回朔法, Time Limit Exceeded
bool backTrackingUsingDPTopDown(vector<int>& nums, int i, vector<explores>& indices) {
if (indices[i] != UNKNOWN) {
return indices[i] == GOON ? true : false;
}
if (i + nums[i] >= static_cast<int>(nums.size()) - 1) {
indices[i] = GOON;
return true;
}
for (int j = nums[i]; j > 0; j--) {
if (backTrackingUsingDPTopDown(nums, j + i, indices)) {
indices[i] = GOON; return true;
}
}
indices[i] = NOWAY; return false;
}
// 2. 动态规划
bool UsingDPBottomUp(vector<int>& nums, vector<explores>& indices) {
indices[nums.size() - 1] = GOON;
for (int i = nums.size() - 2; i >= 0; i--) {
int furestJump = min(nums[i] + i, static_cast<int>(nums.size()) - 1);
for (int j = i + 1; j <= furestJump; j++) {
if (indices[j] == GOON) {
indices[i] = GOON;
break;
}
}
}
return indices[0] == GOON;
}
public:
bool canJump(vector<int>& nums) {
vector<explores> indices(nums.size(), UNKNOWN);
//return UsingDPBottomUp(nums, indices);
return UsingDPBottomUp(nums, indices);
}
};
4. 贪心算法(Greedy)
4.1 解题思路
最后,我们来看看第二个动态规划的方法。我们发现,序号5可以抵到终点,所以我们可以往前找,只要有序号能抵达序号5,就能顺藤摸瓜达到终点。所以序号1可以达到5,之后序号0可以达到序号1,因而必有一条路径直通终点!根据这个思路,我们总结出下面的步骤:
- 初始化TargetIdx = 终点序号,从后往前遍历,如果有nums[i] >= targetIdx - i,则有点必然达到TargetIdx;
- 更新TargetIdx = i,继续向前寻找;
- 结束时如果TargetIdx == 0,说明能从起点序号到终点序号,返回TRUE;反之,返回False;
4.2 实例代码
class Solution {
public:
bool canJump(vector<int>& nums) {
int targetIdx = nums.size() - 1;
for (int i = targetIdx - 1; i >= 0; i--) {
if (nums[i] >= targetIdx - i) {
targetIdx = i;
}
}
return targetIdx == 0;
}
};