【LeetCode】跳跃游戏Ⅰ~Ⅵ 😵😵😵
跳跃游戏★
【题目】给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
【示例】
---输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置
---输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置
【解题思路】
方法一:从左向右
使用right
保存能够到达的最右位置,每访问一个位置,若当前位置小于等于最右位置,说明能够跳到当前位置,然后更新最右位置即可。
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length, right = 0;
for(int i = 0; i < n; i++) {
if(i <= right && nums[i] + i > right) {
right = nums[i] + i;
}
}
return right >= n - 1;
}
}
方法二:从右向左(逆向思维)
使用left
保存能够跳到终点的最左位置,逆向遍历,若当前位置能够跳到left
,则更新left
为当前下标,最后判断能否到达起点即可
class Solution {
public boolean canJump(int[] nums) {
if(nums == null || nums.length < 2) {
return true;
}
int left = nums.length - 1;
for(int i = nums.length - 2; i >= 0; i--) {
if(i + nums[i] >= left) left = i;
}
return left == 0;
}
}
跳跃游戏Ⅱ★★★
【题目】给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
**说明:**假设你总是可以到达数组的最后一个位置。
【示例】
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2,从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置
【解题思路】
方法一:
若当前位置为i
,则它能够跳到的位置区间为[0, nums[i]]
,假如当前跳数为j
,则 i + j
为跳到的位置,此位置可能之前已经跳到过,更新最小跳跃次数即可,状态方程为
d
p
[
i
+
j
]
=
m
i
n
(
d
p
[
i
+
j
]
,
d
p
[
i
]
+
1
)
dp[i + j] = min(dp[i+j], dp[i] + 1)
dp[i+j]=min(dp[i+j],dp[i]+1)
这个思路是很容易想到的,但有一个测试用例会超时
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1000000);
dp[0] = 0;
for(int i = 0; i < n - 1; i++) {
for(int j = 1; j <= nums[i]; j++) {
if(i + j < n) {
dp[i + j] = Math.min(dp[i + j], dp[i] + 1);
}
}
}
return dp[n - 1];
}
}
方法二:
按题目要求一定能够从起点跳到终点,贪心取每一跳达到的最远位置即可。
reach
是当前跳跃的右界限,nextreach
是下一次跳跃的右界限,且nextreach
是一直动态更新的
class Solution {
public int jump(int[] nums) {
int steps = 0;
int reach = 0, nextreach = 0;
for(int i = 0; i < nums.length - 1; i++) {
nextreach = Math.max(nextreach, i + nums[i]);
if(reach == i) {
reach = nextreach;
steps++;
}
}
return steps;
}
}
为了方便理解,对于示例nums = [2,3,1,1,4]
的代码轨迹如下
nums = [2,3,1,1,4]
------------------------------------------
i = 0 reach = 2 nextreach = 2 steps = 1
i = 1 reach = 2 nextreach = 4 steps = 1
i = 2 reach = 4 nextreach = 4 steps = 2
i = 3 reach = 4 nextreach = 4 steps = 2
跳跃游戏Ⅲ★★
【题目】这里有一个非负整数数组 arr
,你最开始位于该数组的起始下标 start
处。当你位于下标 i
处时,你可以跳到 i + arr[i]
或者 i - arr[i]
。
请你判断自己是否能够跳到对应元素值为 0
的 任一 下标处。
注意,不管是什么情况下,你都无法跳到数组之外。
【示例】
---输入:arr = [4,2,3,0,3,1,2], start = 5
输出:true
解释:
到达值为 0 的下标 3 有以下可能方案:
下标 5 -> 下标 4 -> 下标 1 -> 下标 3
下标 5 -> 下标 6 -> 下标 4 -> 下标 1 -> 下标 3
---输入:arr = [3,0,2,1,2], start = 2
输出:false
解释:无法到达值为 0 的下标 1 处
【解题思路】
方法一:深度优先搜索
class Solution {
public boolean canReach(int[] arr, int start) {
return dfs(arr, start);
}
private boolean dfs(int[] arr, int start) {
if(start < 0 || start >= arr.length || arr[start] == -1) {
return false;
}
if(arr[start] == 0) return true;
int l = start + arr[start];
int r = start - arr[start];
arr[start] = -1;
return dfs(arr, l) || dfs(arr, r);
}
}
方法二:广度优先搜索
class Solution {
public boolean canReach(int[] arr, int start) {
boolean[] spot = new boolean[arr.length];
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
spot[start] = true;
while(!queue.isEmpty()) {
int cur = queue.poll();
if(arr[cur] == 0) return true;
int left = cur - arr[cur], right = cur + arr[cur];
if(left >= 0 && !spot[left]) {
queue.offer(left);
spot[left] = true;
}
if(right < arr.length && !spot[right]) {
queue.offer(right);
spot[right] = true;
}
}
return false;
}
}
跳跃游戏 Ⅳ★★★
【题目】给你一个整数数组 arr
,你一开始在数组的第一个元素处(下标为 0)。
每一步,你可以从下标 i
跳到下标:
i + 1
满足:i + 1 < arr.length
i - 1
满足:i - 1 >= 0
- j 满足:
arr[i] == arr[j]
且i != j
请你返回到达数组最后一个元素的下标处所需的 最少操作次数 。
注意:任何时候你都不能跳到数组外面。
【示例】
----输入:arr = [100,-23,-23,404,100,23,23,23,3,404]
输出:3
解释:那你需要跳跃 3 次,下标依次为 0 --> 4 --> 3 --> 9 。下标 9 为数组的最后一个元素的下标。
----输入:arr = [7]
输出:0
解释:一开始就在最后一个元素处,所以你不需要跳跃。
【解题思路】
需要注意的是对于测试用例如 [1, 1, 1, 1, 2 , 2]
可简化为[1, 1, 2, 2]
,同一段连续相同值只需首尾两个元素即可。
方法一:BFS + 动态规划
运行时长100ms,超越7%
class Solution {
public int minJumps(int[] arr) {
int n = arr.length;
Map<Integer, List<Integer>> map = new HashMap<>();
for(int i = 0; i < n; i++) {
//同一段值只需头和尾, 如 1, 1, 1, 1, 3, 3 简化为 1, 1, 3, 3
if(i > 0 && arr[i] == arr[i - 1] && i + 1 < n && arr[i] == arr[i + 1]) {
continue;
}
if(!map.containsKey(arr[i])) {
map.put(arr[i], new ArrayList<Integer>());
}
map.get(arr[i]).add(i);
}
int[] dp = new int[n];
Arrays.fill(dp, Integer.MAX_VALUE);
Deque<Integer> queue = new ArrayDeque<>();
dp[0] = 0;
queue.offer(0);
while(!queue.isEmpty()) {
int i = queue.poll();
//向左跳
if(i - 1 >= 0 && dp[i - 1] > dp[i] + 1) {
queue.offer(i - 1);
dp[i - 1] = dp[i] + 1;
}
//向右跳
if(i + 1 < n && dp[i + 1] > dp[i] + 1) {
queue.offer(i + 1);
dp[i + 1] = dp[i] + 1;
}
//同值跳跃
for(int j = 0; j < map.get(arr[i]).size(); j++) {
int k = map.get(arr[i]).get(j);
if(i != k && dp[i] + 1 < dp[k]) {
queue.offer(k);
dp[k] = dp[i] + 1;
}
}
}
return dp[n - 1];
}
}
方法二:优化BFS
运行时长52ms,超越84%
class Solution {
public int minJumps(int[] arr) {
int n = arr.length;
Map<Integer, List<Integer>> map = new HashMap<>();
for(int i = 0; i < n; i++) {
//同一段值只需头和尾, 如 1, 1, 1, 1, 3, 3 简化为 1, 1, 3, 3
if(i > 0 && arr[i] == arr[i - 1] && i + 1 < n && arr[i] == arr[i + 1]) {
continue;
}
if(!map.containsKey(arr[i])) {
map.put(arr[i], new ArrayList<Integer>());
}
map.get(arr[i]).add(i);
}
int steps = 0;
boolean[] visted = new boolean[n];
Deque<Integer> queue = new LinkedList<>();
visted[0] = true;
queue.offer(0);
while(!queue.isEmpty()) {
int size = queue.size();
for(int i = 0; i < size; i++) {
int j = queue.poll();
if(j == n - 1) return steps;
//向左跳
if(j - 1 >= 0 && !visted[j - 1]) {
queue.offer(j - 1);
visted[j - 1] = true;
}
//向右跳
if(j + 1 < n && !visted[j + 1]) {
queue.offer(j + 1);
visted[j + 1] = true;
}
//同值跳跃
for(int k = 0; k < map.get(arr[j]).size(); k++) {
int next = map.get(arr[j]).get(k);
if(next != j && !visted[next]) {
queue.offer(next);
visted[next] = true;
}
}
}
steps++;
}
return steps;
}
}
跳跃游戏 Ⅴ★★★
【题目】给你一个整数数组 arr
和一个整数 d
。每一步你可以从下标 i
跳到:
i + x
,其中i + x < arr.length
且0 < x <= d
。i - x
,其中i - x >= 0
且0 < x <= d
。
除此以外,你从下标 i
跳到下标 j
需要满足:arr[i] > arr[j]
且 arr[i] > arr[k]
,其中下标 k
是所有 i
到 j
之间的数字(更正式的,min(i, j) < k < max(i, j)
)。
你可以选择数组的任意下标开始跳跃。请你返回你 最多 可以访问多少个下标。
请注意,任何时刻你都不能跳到数组的外面。
【示例】
----输入:arr = [6,4,14,6,8,13,9,7,10,6,12], d = 2
输出:4
解释:你可以从下标 10 出发,然后如上图依次经过 10 --> 8 --> 6 --> 7 。
注意,如果你从下标 6 开始,你只能跳到下标 7 处。你不能跳到下标 5 处因为 13 > 9 。你也不能跳到下标 4 处,因为下标 5 在下标 4 和 6 之间且 13 > 9 。
类似的,你不能从下标 3 处跳到下标 2 或者下标 1 处。
----输入:arr = [3,3,3,3,3], d = 3
输出:1
解释:你可以从任意下标处开始且你永远无法跳到任何其他坐标。
【解题思路】记忆化搜索
class Solution {
private int[] dp;
public int maxJumps(int[] arr, int d) {
int n = arr.length;
dp = new int[n];
int res = 0;
for(int i = 0; i < n; i++) {
res = Math.max(res, dfs(arr, d, i, n));
}
return res;
}
private int dfs(int[] arr, int d, int i, int n) {
if(dp[i] != 0) return dp[i];
dp[i] = 1;
//向左搜索
for(int j = 1; j <= d && i - j >= 0 && arr[i] > arr[i - j]; j++) {
dp[i] = Math.max(dp[i], dfs(arr, d, i - j, n) + 1);
}
//向右搜索
for(int j = 1; j <= d && i + j < n && arr[i] > arr[i + j]; j++) {
dp[i] = Math.max(dp[i], dfs(arr, d, i + j, n) + 1);
}
return dp[i];
}
}
跳跃游戏Ⅵ★★
【题目】给你一个下标从 0 开始的整数数组 nums
和一个整数 k
。
一开始你在下标 0 处。每一步,你最多可以往前跳 k
步,但你不能跳出数组的边界。也就是说,你可以从下标 i
跳到 [i + 1, min(n - 1, i + k)]
包含 两个端点的任意位置。
你的目标是到达数组最后一个位置(下标为 n - 1
),你的 得分 为经过的所有数字之和。
请你返回你能得到的 最大得分 。
【示例】
输入:nums = [1,-1,-2,4,-7,3], k = 2
输出:7
解释:你可以选择子序列 [1,-1,4,3] (上面加粗的数字),和为 7 。
【解题思路】
方法一:常规dp
时间复杂度为O(n²)
,会超时
class Solution {
public int maxResult(int[] nums, int k) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, Integer.MIN_VALUE);
dp[0] = nums[0];
for(int i = 1; i < n; i++) {
for(int j = Math.max(0, i - k); j < i; j++) {
dp[i] = Math.max(dp[i], dp[j]);
}
dp[i] += nums[i];
}
return dp[n - 1];
}
}
方法二:优先队列
class Solution {
public int maxResult(int[] nums, int k) {
int n = nums.length;
Queue<int[]> queue = new PriorityQueue<int[]>((o1, o2) -> {
return o2[1] - o1[1];
});
queue.offer(new int[]{0, nums[0]});
int res = nums[0];
for(int i = 1; i < n; i++) {
while(queue.peek()[0] + k < i) {
queue.poll();
}
res = queue.peek()[1] + nums[i];
queue.offer(new int[]{i, res});
}
return res;
}
}
方法三:单调队列
class Solution {
public int maxResult(int[] nums, int k) {
int n = nums.length;
LinkedList<int[]> queue = new LinkedList<int[]>();
queue.offer(new int[]{0, nums[0]});
int res = nums[0];
for(int i = 1; i < n; i++) {
while(queue.peekFirst()[0] + k < i) {
queue.pollFirst();
}
res = queue.peekFirst()[1] + nums[i];
while(!queue.isEmpty() && queue.peekLast()[1] < res) {
queue.pollLast();
}
queue.offer(new int[]{i, res});
}
return res;
}
}
最少侧跳次数★★
【题目】给你一个长度为n
的3跑道道路 ,它总共包含n + 1
个 点 ,编号为 0
到n
。一只青蛙从0
号点第二条
跑道 **出发 **,它想要跳到点 n
处。然而道路上可能有一些障碍。
给你一个长度为n + 1
的数组obstacles
,其中 obstacles[i]
(取值范围从 0 到 3)表示在点i
处的 obstacles[i]
跑道上有一个障碍。如果obstacles[i] == 0
,那么点i
处没有障碍。任何一个点的三条跑道中 最多有一个 障碍。
-
比方说,如果
obstacles[2] == 1
,那么说明在点2
处跑道1
有障碍。
这只青蛙从点i
跳到点i + 1
且跑道不变的前提是点i + 1
的同一跑道上没有障碍。为了躲避障碍,这只青蛙也可以在 **同一个 **点处 **侧跳 **到 **另外一条 **跑道(这两条跑道可以不相邻),但前提是跳过去的跑道该点处没有障碍。 -
比方说,这只青蛙可以从点
3
处的跑道3
跳到点3
处的跑道1
。
这只青蛙从点0
处跑道 2
出发,并想到达点 n
处的 任一跑道 ,请你返回 最少侧跳次数 。
注意:点0
处和点 n
处的任一跑道都不会有障碍。
提示:
obstacles.length == n + 1
1 <= n <= 5 * 105
0 <= obstacles[i] <= 3
obstacles[0] == obstacles[n] == 0
【示例】
示例1
输入:obstacles = [0,1,2,3,0]
输出:2
解释:最优方案如上图箭头所示。总共有 2 次侧跳(红色箭头)。
注意,这只青蛙只有当侧跳时才可以跳过障碍(如上图点 2 处所示)。
示例2
输入:obstacles = [0,2,1,0,3,0]
输出:2
解释:最优方案如上图所示。总共有 2 次侧跳。
【解题思路】
动态规划
class Solution {
public int minSideJumps(int[] obstacles) {
int n = obstacles.length;
int[][] dp = new int[n + 1][4];
int maxVal = (int)(5 * 1e5 + 100);
for (int[] p : dp) {
Arrays.fill(p, maxVal);
}
dp[1][1] = 1;
dp[1][2] = 0;
dp[1][3] = 1;
for (int i = 2; i <= n; i++) {
if (obstacles[i - 1] != 1) dp[i][1] = dp[i - 1][1];
if (obstacles[i - 1] != 2) dp[i][2] = dp[i - 1][2];
if (obstacles[i - 1] != 3) dp[i][3] = dp[i - 1][3];
if (obstacles[i - 1] != 1) {
dp[i][1] = Math.min(dp[i][1], Math.min(dp[i][2], dp[i][3]) + 1);
}
if (obstacles[i - 1] != 2) {
dp[i][2] = Math.min(dp[i][2], Math.min(dp[i][1], dp[i][3]) + 1);
}
if (obstacles[i - 1] != 3) {
dp[i][3] = Math.min(dp[i][3], Math.min(dp[i][1], dp[i][2]) + 1);
}
}
return Math.min(dp[n][1], Math.min(dp[n][2], dp[n][3]));
}
}
青蛙过河★★★
【题目】
一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。
给你石子的位置列表 stones
(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃一个单位(即只能从单元格 1 跳至单元格 2 )。如果青蛙上一步跳跃了 k
个单位,那么它接下来的跳跃距离只能选择为k - 1
、k
或 k + 1
个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
提示:
2 <= stones.length <= 2000
0 <= stones[i] <= 231 - 1
stones[0] == 0
【示例】
输入:stones = [0,1,3,5,6,8,12,17]
输出:true
解释:青蛙可以成功过河,按照如下方案跳跃:跳 1 个单位到第 2 块石子, 然后跳 2 个单位到第 3 块石子, 接着 跳 2 个单位到第 4 块石子, 然后跳 3 个单位到第 6 块石子, 跳 4 个单位到第 7 块石子, 最后,跳 5 个单位到第 8 个石子(即最后一块石子)。
---------------------------------------------------------------------------------------
输入:stones = [0,1,2,3,4,8,9,11]
输出:false
解释:这是因为第 5 和第 6 个石子之间的间距太大,没有可选的方案供青蛙跳跃过去。
【解题思路】
动态规划,状态转移方程如下
d
p
[
0
]
[
0
]
=
t
r
u
e
dp[0][0]=true
dp[0][0]=true
d p [ i ] [ k ] = ( d p [ j ] [ k − 1 ] ) ∣ ( d p [ j ] [ k ] ) ∣ ( d p [ j ] [ k + 1 ] ) dp[i][k] = (dp[j][k - 1])|(dp[j][k])|(dp[j][k + 1]) dp[i][k]=(dp[j][k−1])∣(dp[j][k])∣(dp[j][k+1])
class Solution {
public boolean canCross(int[] stones) {
int n = stones.length;
if (n < 2) {
return true;
}
if (stones[1] != 1) {
return false;
}
boolean[][] dp = new boolean[n][n + 1];
dp[0][0] = true;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
int k = stones[i] - stones[j];
if (k <= j + 1) {
dp[i][k] = dp[j][k - 1] | dp[j][k] | dp[j][k + 1];
if (i == n - 1 && dp[i][k]) {
return true;
}
}
}
}
return false;
}
}
🎈
白鳯于2021/5/14更新