题目描述
给你一个下标从 0 开始、由 n 个整数组成的数组 nums 和一个整数 target 。你的初始位置在下标 0 。
在一步操作中,你可以从下标 i 跳跃到任意满足下述条件的下标 j:
- 0 <= i < j < n
- -target <= nums[j] - nums[i] <= target
返回到达下标 n - 1 处所需的最大跳跃次数,如果无法到达下标 n - 1 ,返回 -1 。
题目链接
LC 2770 Maximum Number of Jumps to Reach the Last Index
题目思路
这道题对于理解单序列DP 如何从DFS 到记忆化搜索 再到 动态规划递推有很大帮助, 所以我也把我整个思考过程思路全部放在里边。
1. 暴力DFS - 使用全局变量保存最大步数 - 超时
这道题目是跳跃游戏的改进版,思路和跳跃游戏一致,最简单的思路就是利用DFS暴力枚举所有情况,每一次递归中,遍历后继点,当后继点和当前点差值绝对值<target 则进行下一层递归,如果达到终点,则计算最大步数并且更新全局最大值。
class Solution {
int[] n;
int tgt;
int max = -1;
public int maximumJumps(int[] nums, int target) {
// dfs
n = nums;
tgt = target;
dfs(0, 0);
return max;
}
private void dfs(int idx, int step) {
if (idx==n.length-1) {
max = Math.max(max, step);
}
for(int i=idx+1; i<n.length; i++) {
if (Math.abs(n[i]-n[idx])<=tgt) {
dfs(i, step+1);
}
}
}
}
时间复杂度:, 递归每一步都要还要遍历能走的最大步数k;空间复杂度:, 如果不考虑额外开的栈空间。
2. 暴力DFS - 不设置全局变量存储最大步数
想要后续理解如果进行记忆化搜索并且动态规划递推,我们首先需要理解如果实现不加入全局变量的情况下实现DFS暴力搜索。思路也很简单,当递归达到出口; 我们直接返回0,每一层递归方程则可以写成:
代码如下:
private int dfs(int idx) {
if (idx==0) {
return 0;
}
int res = Integer.MIN_VALUE;
for (int i=idx-1; i>=0; i--) {
if (Math.abs(n[i]-n[idx])<=tgt) {
res = Math.max(res, dfs(i)+1);
}
}
return res;
}
3. DFS + 记忆化搜索
能否实现记忆化首先要确定解空间是否存在重复解,当你向后递归时,不同的两个点可能会跳到同一个节点,这时后续的计算就是重复的,所以我们加入单序列 dp用来存储top-down运算时出现的重复情况。
代码如下:
- 需要注意对于dp[] 初始化,定义为从i 到 末尾节点所用的最大步数,我们以-1来表示当前节点没有访问过,如果当前节点无法跳到末尾,那么我们就赋值 Integer.MAX_VALUE表示无法到达。
class Solution {
int[] n;
int tgt;
int[] dp;
int max = -1;
public int maximumJumps(int[] nums, int target) {
// dfs
n = nums;
tgt = target;
dp = new int[n.length]; //i跳到ed最大步数
Arrays.fill(dp, -1);
dp[n.length-1] = 0;
int ans = dfs(0);
return ans<0? -1: ans;
}
/**
*/
private int dfs(int idx) {
if (idx==n.length-1) {
return 0;
}
if (dp[idx]!=-1) return dp[idx]; // 算过了
int res = Integer.MIN_VALUE;
for(int i=idx+1; i<n.length; i++) {
//遍历后面的点找到最大步数更新
if(Math.abs(n[i]-n[idx])<=tgt) {
res = Math.max(dfs(i)+1, res);
}
}
return dp[idx] = res;
}
}
时间复杂度:记忆化时间复杂度比较难估计;空间复杂度:。
4. 动态规划递推
DFS + 记忆化搜索本身就是top-down的动态规划,那么如何将递归转化成递推表达式?整体思路是定义 单序列 dp[i] 为从0到 i 的最大步数,初始化为 -1 表示无法到达该点,当然这里你也可以定义为从 i 到末尾的最大步数,实现方式大同小异。
接下来就是定义递推状态转移方程,我们从左到右遍历每一个点,同时遍历该点可以达到的下一个点,
代码如下:
class Solution {
public int maximumJumps(int[] nums, int target) {
// DP Iteration
int[] dp = new int[nums.length]; // 从0到i最大步数
Arrays.fill(dp, -1);
dp[0] = 0;
for (int i=1; i<nums.length; i++) {
// 外循环i从1 -> n
for (int k=0; k<i; k++) {
//内循环从[1, i]更新所有值
if (Math.abs(nums[k]-nums[i])<=target && dp[k]!=-1) {
// k can jump to i and 0 can jump to k
dp[i] = Math.max(dp[i], dp[k]+1);
}
}
}
return dp[nums.length-1];
}
}
时间复杂度:;空间复杂度:。