给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3步到达最后一个位置。
示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 ,所以你永远不可能到达最后一个位置。
题目链接:中文题目;英文题目
对于这道题,回朔法是非常直接的思路:
当然1. 可以先取最小值,再取最大值,但是这样会稍微慢一些,我们可以优先取最大值,保证每次的移动的距离最大。
可惜的是,这个方法对于某些特殊的test case是会超时的,一般都是右边有0,比如下面两:1) test case1;2)test case2
class Solution {
bool backTracking(vector<int>& nums, int i) {
if (i + nums[i] >= static_cast<int>(nums.size()) - 1) return true; // static_cast - 因为需要用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); }
};
动态规划有两种思路: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即可。具体的流程见下图:
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);
}
};
最后,我们来看看第二个动态规划的方法。我们发现,序号5可以抵到终点,所以我们可以往前找,只要有序号能抵达序号5,就能顺藤摸瓜达到终点。所以序号1可以达到5,之后序号0可以达到序号1,因而必有一条路径直通终点!根据这个思路,我们总结出下面的步骤:
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;
}
};