【Py/Java/C++三种语言OD独家2024D卷真题】20天拿下华为OD笔试之【DP】2024D-跳格子(3)【欧弟算法】全网注释最详细分类最全的华为OD真题题解

34 篇文章 0 订阅
3 篇文章 0 订阅

有LeetCode算法/华为OD考试扣扣交流群可加 948025485
可上 欧弟OJ系统 练习华子OD、大厂真题
绿色聊天软件戳 od1441了解算法冲刺训练(备注【CSDN】否则不通过)

从2024年4月15号开始,OD机考全部配置为2024D卷
注意两个关键点:

  1. 会遇到C卷复用题。虽然可能存在幸存者偏差,但肯定还会有一大部分的旧题。
  2. 现在又支持做完题目之后倒回去改了。就是可以先做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过程比较明显,直接考虑动态规划三部曲:

  1. dp数组的含义是什么?
  • f数组是一个长度为n 的一维列表,f[i]表示跳到第i个格子后,得到的最大得分数。
  1. 动态转移方程是什么?
  • 现在相当于求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终止。
  1. 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了解更多

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值