题目链接

时空漫游者的能量跳跃

题目描述

一位时空漫游者需要从路径的起点信标 跳跃到终点信标 。路径上共有 个信标,每个信标 拥有能量值

漫游者从信标 出发,下一步可以跳跃到区间 内的任意一个信标,其中 是最大跳跃距离。

任务是规划一条从信标 到信标 的路径,使得收集到的总能量最大化。

输入:

  • 第一行是最大跳跃距离
  • 第二行是信标总数
  • 第三行是 个整数,代表每个信标的能量值

输出:

  • 一个整数,代表能收集到的最大总能量。

解题思路

这是一个典型的动态规划问题,旨在寻找一条最优路径以获得最大累积值。

状态定义

我们定义一个数组 ,其中 表示跳跃到第 个信标时能收集到的最大总能量。我们的最终目标是计算出

状态转移方程

要计算 ,我们需要考虑所有可能的前驱信标。根据题目规则,我们可以从位于区间 内的任意信标 跳跃到信标 。为了使到达 的总能量最大化,我们应该从具有最大累积能量的那个前驱信标 跳过来。因此,状态转移方程为:

基础情况

旅程从信标 开始,所以

优化

直接根据状态转移方程计算的时间复杂度为 ,因为对于每个 ,都需要遍历 个前驱状态。当 很大时,这可能会超时。

我们可以观察到,计算 的过程实际上是在一个大小为 的滑动窗口内寻找最大值。这个问题可以使用 单调队列 (Monotonic Queue) 进行优化。

我们使用一个双端队列(deque),它将存储信标的索引。该队列将始终保持其对应 值的单调递减性(队首的 值最大)。

  1. 在计算 时,首先从队首移除所有不在 窗口内的索引。
  2. 此时,队首的索引 对应的 就是当前窗口内的最大值。我们用它来计算
  3. 为了维护队列的单调性,从队尾开始,移除所有 值小于或等于新计算出的 的索引。
  4. 将当前索引 加入队尾。

通过这种方式,每个索引最多入队和出队一次,寻找窗口最大值的时间复杂度从 降为均摊

代码

#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>

using namespace std;

int main() {
    int k, n;
    cin >> k;
    cin >> n;
    vector<long long> E(n);
    for (int i = 0; i < n; ++i) {
        cin >> E[i];
    }

    if (n == 0) {
        cout << 0 << endl;
        return 0;
    }

    vector<long long> dp(n);
    deque<int> q; // 单调队列,存储索引

    dp[0] = E[0];
    q.push_back(0);

    for (int i = 1; i < n; ++i) {
        // 移除窗口外的旧索引
        if (!q.empty() && q.front() < i - k) {
            q.pop_front();
        }
        
        // 计算 dp[i],队首元素是窗口内的最大值
        dp[i] = E[i] + dp[q.front()];

        // 维护队列的单调递减性
        while (!q.empty() && dp[q.back()] <= dp[i]) {
            q.pop_back();
        }
        q.push_back(i);
    }

    cout << dp[n - 1] << endl;

    return 0;
}
import java.util.Scanner;
import java.util.Deque;
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int k = sc.nextInt();
        int n = sc.nextInt();
        long[] E = new long[n];
        for (int i = 0; i < n; i++) {
            E[i] = sc.nextLong();
        }

        if (n == 0) {
            System.out.println(0);
            return;
        }

        long[] dp = new long[n];
        Deque<Integer> q = new LinkedList<>(); // 单调队列,存储索引

        dp[0] = E[0];
        q.addLast(0);

        for (int i = 1; i < n; i++) {
            // 移除窗口外的旧索引
            if (!q.isEmpty() && q.peekFirst() < i - k) {
                q.removeFirst();
            }

            // 计算 dp[i],队首元素是窗口内的最大值
            dp[i] = E[i] + dp[q.peekFirst()];

            // 维护队列的单调递减性
            while (!q.isEmpty() && dp[q.peekLast()] <= dp[i]) {
                q.removeLast();
            }
            q.addLast(i);
        }

        System.out.println(dp[n - 1]);
    }
}
import collections

# 读取输入
k = int(input())
n = int(input())
E = list(map(int, input().split()))

if n == 0:
    print(0)
else:
    dp = [0] * n
    q = collections.deque() # 单调队列,存储索引

    dp[0] = E[0]
    q.append(0)

    for i in range(1, n):
        # 移除窗口外的旧索引
        if q and q[0] < i - k:
            q.popleft()
        
        # 计算 dp[i],队首元素是窗口内的最大值
        dp[i] = E[i] + dp[q[0]]

        # 维护队列的单调递减性
        while q and dp[q[-1]] <= dp[i]:
            q.pop()
        q.append(i)

    print(dp[n - 1])

算法及复杂度

  • 算法:动态规划 + 单调队列优化
  • 时间复杂度:,因为每个信标的索引最多入队和出队一次。
  • 空间复杂度:,主要用于存储 数组。单调队列的空间复杂度为