Leetcode 1696 跳跃游戏 VI
1696.跳跃游戏 VI
给你一个下标从 0 开始的整数数组 nums
和一个整数 k
。
一开始你在下标 0
处。每一步,你最多可以往前跳 k
步,但你不能跳出数组的边界。也就是说,你可以从下标 i
跳到 [i + 1, min(n - 1, i + k)]
包含 两个端点的任意位置。
你的目标是到达数组最后一个位置(下标为 n - 1
),你的 得分 为经过的所有数字之和。
请你返回你能得到的 最大得分 。
示例 1:
输入: nums = [**1**,**-1**,-2,**4**,-7,**3**], k = 2
输出: 7
解释: 你可以选择子序列 [1,-1,4,3]
(上面加粗的数字),和为 7 。
示例 2:
输入: nums = [**10**,-5,-2,**4**,0,**3**], k = 3
输出: 17
解释: 你可以选择子序列 [10,4,3]
(上面加粗数字),和为 17 。
示例 3:
输入: nums = [1,-5,-20,4,-1,3,-6,-3], k = 2
输出: 0
提示:
1 <= nums.length, k <= 10^5
-10^4 <= nums[i] <= 10^4
Related Topics
- 队列
- 数组
- 动态规划
- 单调队列
- 堆(优先队列)
题目链接: link
解答
本题的难度是 Medium.
昨天的是简单, 还以为今天的会是困难的. 这个题目很明显地可以用动态规划来解决, 因此我直接尝试一下, 就是看前面 [i-k, i-1]
到当前值的最大值, 为了避免第一次的时候 dp
的值可能是 0, 但是最大值是负数, 因此我直接初始化为 Integer.MIN_VALUE
, 后面发现实际上这个根本不必要.
代码如下:
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 = 1; j <= k; j++) {
if(i-j>=0){
dp[i] = Math.max(dp[i], dp[i-j]+nums[i]);
} else {
break;
}
}
}
System.out.println(Arrays.toString(dp));
return dp[n-1];
}
}
此时的时间复杂度为 O(n*k)
, 结果超时了, k=94270, 然后数组超级大的一个样例. 不仅会超时而且很抽象的是其实根本不需要这么一个初始化dp的过程, 因为其实本质就是看前面 k
个数的最大值是多少就行了, 后面加的值都是固定的, 那我们考虑降低时间复杂度, 看看能不能稳定地维护一个前 k 个值的最大值. 因此问题就转化为了求一个滑动窗口内的最大值. 我们通过单调队列去维护一个单调递减的队列, 这样就可以保证队头的元素是最大的, 然后我们每次都去维护这个队列, 使得队列的长度不超过 k
, 这样就可以得到最大值了. 代码如下:
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];
Deque<Integer> deque = new ArrayDeque<>();
deque.offer(0);
for (int i = 1; i < n; i++) {
// 先判断队头的元素是不是在滑动窗口范围内
while(!deque.isEmpty() && deque.peek() <i-k){
deque.poll();
}
// 现在都在队内了, 因此就可以判断了
dp[i] = dp[deque.peek()]+nums[i];
// 现在得到了新的值就去维护单调
while(!deque.isEmpty() && dp[deque.peekLast()] < dp[i]){
deque.pollLast();
}
deque.offer(i);
}
return dp[n-1];
}
}
时间开销为 26ms, 击败了 47.47% 的提交, 只击败了不到一半的提交, 按照以往的经验是因为用了这种自带的类导致的, 我们可以看到, 其实我们的队列里面存的是下标, 而不是值, 因此我们可以用数组来模拟这个队列, 这样就可以减少一些时间开销. 代码如下:
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];
int start = 0;
int end = 0;
int[] deque = new int[n+1];
deque[start] = 0;
for (int i = 1; i < n; i++) {
// 先判断队头的元素是不是在范围内
while(start <= end && deque[start] <i-k){
start++;
}
// 现在都在队内了, 因此就可以判断了
dp[i] = dp[deque[start]]+nums[i];
// 现在得到了新的值就去维护单调
while(start <= end && dp[deque[end]] < dp[i]){
end--;
}
end++;
deque[end] = i;
}
return dp[n-1];
}
}
时间开销为 7ms, 击败了 93.04%, 暂时想不到什么方法可以更快了, 结果一看别人的方法, 维护一个最大值, 并记下他的下标, 当下标不在范围内的时候就重新遍历一遍前面的 k 个值, 找到最大值. (大概这个思路)
没想到这么简单粗暴的方式能够达到这样的时间效率.