题目链接
题目描述
一位时空漫游者需要从路径的起点信标 跳跃到终点信标
。路径上共有
个信标,每个信标
拥有能量值
。
漫游者从信标 出发,下一步可以跳跃到区间
内的任意一个信标,其中
是最大跳跃距离。
任务是规划一条从信标 到信标
的路径,使得收集到的总能量最大化。
输入:
- 第一行是最大跳跃距离
。
- 第二行是信标总数
。
- 第三行是
个整数,代表每个信标的能量值
。
输出:
- 一个整数,代表能收集到的最大总能量。
解题思路
这是一个典型的动态规划问题,旨在寻找一条最优路径以获得最大累积值。
状态定义
我们定义一个数组 ,其中
表示跳跃到第
个信标时能收集到的最大总能量。我们的最终目标是计算出
。
状态转移方程
要计算 ,我们需要考虑所有可能的前驱信标。根据题目规则,我们可以从位于区间
内的任意信标
跳跃到信标
。为了使到达
的总能量最大化,我们应该从具有最大累积能量的那个前驱信标
跳过来。因此,状态转移方程为:
基础情况
旅程从信标 开始,所以
。
优化
直接根据状态转移方程计算的时间复杂度为 ,因为对于每个
,都需要遍历
个前驱状态。当
和
很大时,这可能会超时。
我们可以观察到,计算 的过程实际上是在一个大小为
的滑动窗口内寻找最大值。这个问题可以使用 单调队列 (Monotonic Queue) 进行优化。
我们使用一个双端队列(deque),它将存储信标的索引。该队列将始终保持其对应 值的单调递减性(队首的
值最大)。
- 在计算
时,首先从队首移除所有不在
窗口内的索引。
- 此时,队首的索引
对应的
就是当前窗口内的最大值。我们用它来计算
。
- 为了维护队列的单调性,从队尾开始,移除所有
值小于或等于新计算出的
的索引。
- 将当前索引
加入队尾。
通过这种方式,每个索引最多入队和出队一次,寻找窗口最大值的时间复杂度从 降为均摊
。
代码
#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])
算法及复杂度
- 算法:动态规划 + 单调队列优化
- 时间复杂度:
,因为每个信标的索引最多入队和出队一次。
- 空间复杂度:
,主要用于存储
数组。单调队列的空间复杂度为
。