LC 45. 跳跃游戏 II

题目描述

这是 LeetCode 上的 45. 跳跃游戏 II ,难度为 中等

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

假设你总是可以到达数组的最后一个位置。

示例 1:

1
2
3
4
5
6
输入: [2,3,1,1,4]

输出: 2

解释: 跳到最后一个位置的最小跳跃数是 2。
  从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例 2:
1
2
3
输入: [2,3,0,1,4]

输出: 2

提示:

  • $1 <= nums.length <= 1000$
  • $0 <= nums[i] <= 10^5$

BFS

对于这一类问题,我们一般都是使用 BFS 进行求解。

本题的 BFS 解法的复杂度是 $O(n^2)$,数据范围为 $10^3$,可以过。

Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int jump(int[] nums) {
int n = nums.length, ans = 0;
boolean[] st = new boolean[n];
Deque<Integer> d = new ArrayDeque<>();
st[0] = true;
d.addLast(0);
while (!d.isEmpty()) {
int size = d.size();
while (size-- > 0) {
int idx = d.pollFirst();
if (idx == n - 1) return ans;
for (int i = idx + 1; i <= idx + nums[idx] && i < n; i++) {
if (!st[i]) {
st[i] = true;
d.addLast(i);
}
}
}
ans++;
}
return ans;
}
}

C++ 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size(), ans = 0;
bool st[n];
memset(st, false, sizeof(st));
deque<int> d;
st[0] = true;
d.push_back(0);
while (!d.empty()) {
int size = d.size();
while (size-- > 0) {
int idx = d.front();
d.pop_front();
if (idx == n - 1) return ans;
for (int i = idx + 1; i <= idx + nums[idx] && i < n; i++) {
if (!st[i]) {
st[i] = true;
d.push_back(i);
}
}
}
ans++;
}
return ans;
}
};

Python 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def jump(self, nums: List[int]) -> int:
n, ans = len(nums), 0
st = [False] * n
d = deque([0])
st[0] = True
while d:
size = len(d)
while size > 0:
idx = d.popleft()
if idx == n - 1:
return ans
for i in range(idx + 1, min(idx + nums[idx] + 1, n)):
if not st[i]:
st[i] = True
d.append(i)
size -= 1
ans += 1
return ans

  • 时间复杂度:如果每个点跳跃的距离足够长的话,每次都会将当前点「后面的所有点」进行循环入队操作(由于 st 的存在,不一定都能入队,但是每个点都需要被循环一下)。复杂度为 $O(n^2)$
  • 空间复杂度:队列中最多有 $n - 1$ 个元素。复杂度为 $O(n)$

双指针 + 贪心 + 动态规划

本题数据范围只有 $10^3$,所以 $O(n^2)$ 勉强能过。

如果面试官要将数据范围出到 $10^6$,又该如何求解呢?

我们需要考虑 $O(n)$ 的做法。

其实通过 $10^6$ 这个数据范围,就已经可以大概猜到是道 DP 题。

我们定义 $f[i]$ 为到达第 $i$ 个位置所需要的最少步数,那么答案是 $f[n - 1]$。

学习过 路径 DP 专题 的同学应该知道,通常确定 DP 的「状态定义」有两种方法。

  • 一种是根据经验猜一个状态定义,会结合题目给定的维度,和要求的答案去猜。

  • 另外一种则是通过设计一个合理的 DFS 方法签名来确定状态定义。

这里我是采用第一种方法。

至于如何确定「状态定义」是否可靠,关键是看使用这个状态定义能否推导出合理的「状态转移方程」,来覆盖我们所有的状态。

不失一般性的考虑 $f[n - 1]$ 该如何转移:

我们知道最后一个点前面可能会有很多个点能够一步到达最后一个点。

也就是有 $f[n - 1] = min(f[n - k],…,f[n - 3],f[n - 2]) + 1$。

然后我们再来考虑集合 $f[n - k],…,f[n - 3],f[n - 2]$ 有何特性。

不然发现其实必然有 $f[n - k] <= …<= f[n - 3] <= f[n - 2]$。

推而广之,不止是经过一步能够到达最后一个点的集合,其实任意连续的区间都有这个性质。

举个🌰,比如我经过至少 5 步到达第 $i$ 个点,那么必然不可能出现使用步数少于 5 步就能达到第 $i + 1$ 个点的情况。到达第 $i + 1$ 个点的至少步数必然是 5 步或者 6 步。

搞清楚性质之后,再回头看我们的状态定义:$f[i]$ 为到达第 $i$ 个位置所需要的最少步数。

因此当我们要求某一个 $f[i]$ 的时候,我们需要找到最早能够经过一步到达 $i$ 点的 $j$ 点。

即有状态转移方程:$f[i] = f[j] + 1$。

也就是我们每次都贪心的取离 $i$ 点最远的点 $j$ 来更新 $f[i]$。

而这个找 $j$ 的过程可以使用双指针来找。

因此这个思路其实是一个「双指针 + 贪心 + 动态规划」的一个解法。

Java 代码:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int[] f = new int[n];
for (int i = 1, j = 0; i < n; i++) {
while (j + nums[j] < i) j++;
f[i] = f[j] + 1;
}
return f[n - 1];
}
}

C++ 代码:
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, 0);
for (int i = 1, j = 0; i < n; i++) {
while (j + nums[j] < i) j++;
f[i] = f[j] + 1;
}
return f[n - 1];
}
};

Python 代码:
1
2
3
4
5
6
7
8
9
10
class Solution:
def jump(self, nums: List[int]) -> int:
n = len(nums)
f = [0] * n
for i in range(1, n):
j = 0
while j + nums[j] < i:
j += 1
f[i] = f[j] + 1
return f[-1]

  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(n)$

最后

这是我们「刷穿 LeetCode」系列文章的第 No.45 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。