LeetCode跳跃游戏
Posted 有顶天の甲子园
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode跳跃游戏相关的知识,希望对你有一定的参考价值。
给定一组非负整数,初始时处于数组的第一位下标 0 的位置,数组的每个元素代表那个位置可以跳跃的最大长度。判断你是否能够到达数组的最后一位下标。
e.g.
A = [2, 3, 1, 1, 4],返回 true。
A = [3, 2, 1, 0, 4],返回 false。
我的想法是递归
方法一:
1 bool canJump(vector<int>& nums) { 2 return jump(nums, 0); 3 } 4 5 bool jump(vector<int> &nums, int m) { 6 int last = nums.size() - 1; 7 if (m == last) return true; 8 int p = min(last, m + nums[m]); 9 for (int i = p; i > m; i--) { // 若不定义 p,i 初始为 m + nums[m] 并不会造成数组越界,但变量 p 减少了不必要的递归 10 if (jump(nums, i)) 11 return true; 12 } 13 return false; 14 }
答案称这种方法是递归回溯法,时间复杂度为 O(2n),数据量很大时会超时。
答案提供了多种解法
方法二:
自顶向下动态规划法(优化的回溯法)
作如下定义:如果从数组中某一位置作为起始点,最终能够到达最后一个下标处,则把这个位置称为 “ Good Index ”,否则成为 “ Bad Index ”。因此这个跳跃问题就变成了判断 0 下标是不是一个 “ Good Index ”。
用一个 memo 数组存储原数组每个下标是好的还是坏的,memo 数组元素的值是 GOOD、BAD、UNKNOWN 之一。
e.g. 对于 nums = [2, 4, 2, 1, 0, 2, 0],
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
nums | 2 | 4 | 2 | 1 | 0 | 2 | 0 |
memo | G | G | B | B | B | G | G |
步骤:
- 初始时,memo 中所有元素都是 UNKNOWN,除了最后一个元素是 GOOD。
- 将回溯法中递归的第一步改为 “ 判断当前下标是否 UNKNOWN ”
如果是 KNOWN,根据其值是 GOOD / BAD 返回 true / false;
否则继续执行回溯。 - 一旦知道当前下标是好的还是坏的,将相应的值存在 memo 数组中。
C++实现:
1 enum Index { 2 GOOD, BAD, UNKNOWN 3 }; 4 5 vector<Index> memo; 6 7 bool canJump(vector<int>& nums) { 8 memo.reserve(nums.size()); 9 for (int i = 0; i < nums.size() - 1; i++) { 10 memo[i] = UNKNOWN; 11 } 12 memo[nums.size() - 1] = GOOD; 13 return jump(nums, 0); 14 } 15 16 bool jump(vector<int> &nums, int m) { 17 if (memo[m] != UNKNOWN) 18 return memo[m] == GOOD ? true : false; 19 int p = min((int)nums.size() - 1, m + nums[m]); 20 for (int i = p; i > m; i--) { 21 if (jump(nums, i)) { 22 memo[m] = GOOD; 23 return true; 24 } 25 } 26 memo[m] = BAD; 27 return false; 28 }
对于数组中每个位置 i,我们在其右边 nums[i] 个元素中寻找 “ Good Index ”,因此这种方法的时间复杂度是 O(n2)。
方法三:
自底向上动态规划法
将自顶向下 DP 的递归消除后就变成了自底向上 DP,这样就不会造成方法栈的过度开销。消除递归的方法是从数组最右边(倒数第二位)往左不断地递推 memo 数组的值。
C++实现:
1 enum Index { 2 GOOD, BAD, UNKNOWN 3 }; 4 5 vector<Index> memo; 6 7 bool canJump(vector<int>& nums) { 8 memo.reserve(nums.size()); 9 for (int i = 0; i < nums.size() - 1; i++) { 10 memo[i] = UNKNOWN; 11 } 12 memo[nums.size() - 1] = GOOD; 13 14 for (int i = nums.size() - 2; i >= 0; i--) { 15 int p = min((int)nums.size() - 1, i + nums[i]); 16 for (int j = p; j > i; j--) { 17 if (memo[j] == GOOD) { 18 memo[i] = GOOD; 19 break; 20 } 21 } 22 } 23 24 return memo[0] == GOOD; 25 }
由于两种 DP 的原理相同,这种方法的时间复杂度也是 O(n2)。由于没有使用递归,终于 Accepted 了,但效率极低。
最优方法:
贪心法
自底向上 DP 中,从后向前递推 memo 数组时,对于每一个下标 i,我们想知道是否能从这个位置到达一个 “ Good Index ”,而当我们找到了从 i 下标能到达的下标范围 ( i, p ] 内最右边的仅仅一个 “ Good Index ”(也可以选择最左边的一个)就把 i 设为 “ Good Index ”。因此,可以把从后向前追踪的一系列 “ Good Index ” 只用一个单独的变量存储,而不是记录在 memo 数组中。
C++实现:
1 bool canJump(vector<int>& nums) { 2 int last = nums.size() - 1; 3 for (int i = last; i >= 0; i--) { 4 if (i + nums[i] >= last) 5 last = i; 6 } 7 return last == 0; 8 }
也可以从前向后
1 bool canJump(vector<int>& nums) { 2 int n = nums.size(), reach = 0; // reach 表示从 0 能到达的最远的下标 3 for (int i = 0; i < n && i <= reach; i++) { 4 reach = max(reach, i + nums[i]); 5 if (reach >= n - 1) 6 return true; 7 } 8 return false; 9 }
以上是关于LeetCode跳跃游戏的主要内容,如果未能解决你的问题,请参考以下文章