有LeetCode算法/华为OD考试扣扣交流群可加 948025485
可上 欧弟OJ系统 练习华子OD、大厂真题
绿色聊天软件戳od1441
了解算法冲刺训练(备注【CSDN】否则不通过)
从2024年4月15号开始,OD机考全部配置为2024D卷。
注意两个关键点:
- 会遇到C卷复用题。虽然可能存在幸存者偏差,但肯定还会有一大部分的旧题。
- 现在又支持做完题目之后倒回去改了。就是可以先做200的再做100的,然后可以反复提交。
题目描述与示例
题目描述
小明和朋友们一起玩跳格子游戏,每个格子上有特定的分数。
比如,score[]=[1,-1,-6,7,-17,7]
,从起点score[0]
开始,每次最大跳的步长为k
,请你返回小明跳到终点score[n-1]
时,能得到的最大得分。
注:
- 格子的总长度和步长的区间在
[1,100000]
- 每个格子的分数在
[-10000,10000]
区间中;
输入描述
第一行输入总的格子数量n
第二行输入每个格子的分数score[]
第三行输入最大跳的步长k
输出描述
输出最大得分数
示例
输入
6
1 -1 -6 7 -17 7
2
输出
14
说明
小明从起点score[0]
开始跳,第一次跳score[1]
,第二次跳到score[3]
,第三次跳到score[5]
,因此得到的最大的得分是score[0]+ score[1]+ score[3]+ score[5]= 14
解题思路
注意,本题和LeetCode. 1696跳跃游戏IV完全一致。
本题的dp过程比较明显,直接考虑动态规划三部曲:
dp
数组的含义是什么?
f
数组是一个长度为n
的一维列表,f[i]
表示跳到第i个格子后,得到的最大得分数。
- 动态转移方程是什么?
- 现在相当于求f[i]的值,如果当前站在格子第i个格子中,那么我可以从第
i-1,i-2,...,i-k
个格子走过来(如果i<=k
,那么可以直接从起点走来)。并且,从中取一个最大值即可。因此状态转移方程为
f[i] = max(f[i-1],f[i-2],f[i-3],...,f[i-k]) + score[i]。如果i <= k,那么取到0终止。
dp
数组如何初始化?
- 一开始我们站在起点,此时已经获得了
f[0] = score[0]
的分数。 - 对于其他值,我们需要取到无穷小,以使得接下来计算的过程中取得最大值。
inf = 10 ** 9
f = [-inf for _ in range(n)]
f[0] = score[0]
不难知道,f[n-1]
就是最终结果。
如下是直接求解f
的python代码:
n = int(input())
score = list(map(int, input().split()))
k = int(input())
inf = 10 ** 9
f = [-inf for i in range(n)]
f[0] = score[0]
for i in range(1, n):
for j in range(1, k + 1):
if j <= i:
f[i] = max(f[i], f[i - j] + score[i])
print(f[-1])
需要注意的是,上面直接求解f数组需要产生O(nk)
的时间。注意到本题的k和n都是100000级别的,因此我们需要考虑优化。
注意我们的状态转移方程其中的一部分:max(f[i-1],f[i-2],f[i-3],...,f[i-k])
,随着i的增长,它询问的区间中,左端点和右端点都只会向右移动(不会向左移动)。每次询问的都是这一类连续区间。我们将借鉴Leetcode 239滑动窗口最大值的思想求解本题。其基本思想是解决使用单调队列。
单调队列是什么?是一个双端队列。和单调栈有点类似,我们通常对单调栈中的栈顶进行操作,单调队列的队尾和单调栈的栈顶操作相似。单调栈和单调队列最大的区别在于,单调栈是不对栈底进行操作的,而单调队列**【只会】**从队头弹出一些元素。什么时候弹出呢?我们不需要的时候就弹出它们。
回到本题。我们的询问区间的左端点和右端点是绝对不会向左移动的,这给单调队列的发挥提供了空间。假设现在k=4,我们已经知道了值f[1], f[2], f[3], f[4], f[5]
,并且我们也求解出了f[5] = max(f[1], f[2], f[3], f[4]) + a[5]
。现在我们需要求解f[6] = max(f[2], f[3], f[4], f[5]) + a[6]
。这里,如果我们已经知道了f[3] < f[4],那么状态f[3]是不是就没有必要需要了呢?
当我们即将新添加的状态比队尾中的旧状态更优时,那么这些旧状态是不是就没必要存在了?因此我们将它们从队尾弹出来,直到队列为空,或者是队尾的状态比新插入的状态更优就好。因此,队列从头到尾,状态是一个比一个差的。这也就意味着,队头的状态q[0]
才是最优的,因此我们不难写出如下代码:
# 维持队列的单调递减性,保证队首始终是当前窗口内最大得分的位置索引
while len(q) > 0 and f[i] >= f[q[-1]]:
q.pop()
# 将当前位置索引添加到队列尾部
q.append(i)
但是,随着询问区间的左端点的推移,一些最优的状态我们是不计入的,因为如果它在询问区间左端点的左侧,那么这些状态就”过期“了,我们就不再需要这些状态,因此这些状态需要从队头弹出,我们可以写出如下代码:
# 如果队首的元素不在当前i位置的k步范围内,从队列中移除
while len(q) > 0 and i - q[0] > k:
q.popleft()
反正,在求解dp数组中,我们需要维护好需要询问的左端点和右端点的位置,让过期的状态和不优的状态淘汰,保留队列中最优秀的状态即可。
代码
python
# 题目:【DP】2024D-跳格子(3)
# 分值:200
# 作者:黄老师-Chiya
# 算法:DP
# 代码看不懂的地方,请直接在群上提问
from collections import deque
# 读取格子的数量
n = int(input())
# 读取每个格子的分数
a = list(map(int, input().split()))
# 读取小明每次跳跃的最大步长
k = int(input())
# 设定无穷大值,用于初始化最大得分数组中的非起点位置
inf = 10 ** 9
# 初始化最大得分数组,所有位置初始化为负无穷大,除了起点
f = [-inf for i in range(n)]
# 创建一个双端队列,用于存储当前滑动窗口内的最优得分的位置索引
q = deque([0])
# 起点的最大得分即为起点的分数
f[0] = a[0]
# 遍历每个格子,计算到达每个格子的最大得分
for i in range(1, n):
# 如果队首的元素不在当前i位置的k步范围内,从队列中移除
while len(q) > 0 and i - q[0] > k:
q.popleft()
# 更新到达当前格子i的最大得分
f[i] = f[q[0]] + a[i]
# 维持队列的单调递减性,保证队首始终是当前窗口内最大得分的位置索引
while len(q) > 0 and f[i] >= f[q[-1]]:
q.pop()
# 将当前位置索引添加到队列尾部
q.append(i)
# 输出到达最后一个格子的最大得分
print(f[-1])
java
import java.util.Scanner;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读入格子的总数
int n = scanner.nextInt();
// 读入每个格子的分数
int[] scores = new int[n];
for (int i = 0; i < n; i++) {
scores[i] = scanner.nextInt();
}
// 读入最大跳跃步长k
int k = scanner.nextInt();
// 初始化动态规划数组,用来存储到达每个格子的最大得分
int[] dp = new int[n];
Arrays.fill(dp, Integer.MIN_VALUE); // 所有元素初始化为最小整数值
dp[0] = scores[0]; // 起点的得分就是起点格子的分数
// 使用双端队列来维持一个滑动窗口
Deque<Integer> deque = new LinkedList<>();
deque.addLast(0); // 起点索引加入队列
// 遍历每个格子,根据动态规划状态转移方程更新dp值
for (int i = 1; i < n; i++) {
// 移除不在有效范围内的索引
if (!deque.isEmpty() && deque.peekFirst() < i - k) {
deque.pollFirst();
}
// 计算到达当前格子的最大得分
dp[i] = dp[deque.peekFirst()] + scores[i];
// 维持队列的单调递减性
while (!deque.isEmpty() && dp[i] >= dp[deque.peekLast()]) {
deque.pollLast();
}
// 将当前格子索引加入队列
deque.addLast(i);
}
// 输出到达最后一个格子的最大得分
System.out.println(dp[n - 1]);
scanner.close();
}
}
cpp
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
int main() {
// 输入格子的数量
int n;
cin >> n;
// 输入每个格子的分数
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
// 输入最大跳跃步长
int k;
cin >> k;
// 定义一个足够大的数表示负无穷
const int inf = 1e9;
// 初始化最大得分数组,所有位置初始化为负无穷大,除了起点
vector<int> f(n, -inf);
f[0] = a[0];
// 创建一个双端队列,用于存储当前滑动窗口内的最优得分的位置索引
deque<int> q;
q.push_back(0);
// 遍历每个格子,计算到达每个格子的最大得分
for (int i = 1; i < n; ++i) {
// 如果队首的元素不在当前i位置的k步范围内,从队列中移除
while (!q.empty() && i - q.front() > k) {
q.pop_front();
}
// 更新到达当前格子i的最大得分
f[i] = f[q.front()] + a[i];
// 维持队列的单调递减性,保证队首始终是当前窗口内最大得分的位置索引
while (!q.empty() && f[i] >= f[q.back()]) {
q.pop_back();
}
// 将当前位置索引添加到队列尾部
q.push_back(i);
}
// 输出到达最后一个格子的最大得分
cout << f[n - 1] << endl;
return 0;
}
时空复杂度
时间复杂度:O(N)
。每个元素最多会进入一次队列,离开一次队列。
空间复杂度:O``(N)
。队列和数组所需要开辟的空间。
华为OD算法/大厂面试高频题算法练习冲刺训练
-
华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务300+同学成功上岸!
-
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
-
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
-
60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁
-
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
-
可查看链接 大厂真题汇总 & OD真题汇总(持续更新)
-
绿色聊天软件戳
od1336
了解更多