题目描述
小R计划一次从地点 A 到地点 B 的徒步旅行,总路程需要 ( N ) 天。为了在路途中保持充足的能量,小R每天必须消耗 1 份食物。幸运的是,小R每天会经过一个补给站,可以购买食物进行补充。然而:
- 每个补给站的食物价格不同。
- 小R最多只能同时携带 ( K ) 份食物。
目标是计算完成这次旅程所需的最低花费。
问题分析
-
核心约束:
- 每天必须消耗 1 份食物,不能出现断粮的情况。
- 小R最多只能携带 ( K ) 份食物,因此需要规划补充食物的时间和数量,以尽量利用低价的补给站。
-
优化目标:
- 在保证每天有食物的前提下,使得总花费最小。
解题思路
我们需要维护一个滑动窗口,表示未来 ( K ) 天内小R可以选择的补给站价格范围。每次在窗口中选择最低价格的补给站进行补充,确保最低花费。
滑动窗口的操作
-
窗口更新:
每天移动窗口,移入当前天的价格,移出超过窗口范围的价格。 -
最小值查询:
每天从窗口中选择价格最低的补给站进行购买。
Deque 的作用
Deque 是 Java 中的双端队列,支持高效的头尾操作,特别适合动态维护窗口中的最小值:
- 保持队列中的元素单调递增(队头为最小值)。
- 移入新元素时,移除队列尾部所有比当前价格高的元素,保证队列单调性。
- 窗口超出范围时,从队头移除过期元素。
算法步骤
-
初始化一个双端队列
Deque,用于存储窗口中的有效价格索引。 -
遍历每一天的价格:
- 移除队头过期的元素(超出窗口范围)。
- 移除队尾所有大于当前价格的元素,保持队列单调性。
- 将当前价格的索引加入队尾。
- 取队头对应的价格作为当天的购买价格,并累加到总花费中。
-
最终输出总花费。
Java 实现
import java.util.Deque;
import java.util.LinkedList;
public class MinCostToTravel {
public static int solution(int n, int k, int[] data) {
int minMoney = 0; // 总花费
Deque<Integer> deque = new LinkedList<>(); // 单调队列,用于维护窗口中的最小值索引
for (int i = 0; i < n; i++) {
// 移除队头过期元素(超出窗口范围)
if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 移除队尾所有比当前价格大的元素,保持单调递增
while (!deque.isEmpty() && data[deque.peekLast()] > data[i]) {
deque.pollLast();
}
// 将当前价格索引加入队尾
deque.offerLast(i);
// 累加当天的最低价格(队头对应的价格)
minMoney += data[deque.peekFirst()];
}
return minMoney;
}
public static void main(String[] args) {
// 测试样例
System.out.println(solution(5, 2, new int[]{1, 2, 3, 3, 2})); // 输出:9
System.out.println(solution(6, 3, new int[]{4, 1, 5, 2, 1, 3})); // 输出:9
System.out.println(solution(4, 1, new int[]{3, 2, 4, 1})); // 输出:10
}
}
代码逻辑详解
-
维护窗口的最小值:
- 单调队列通过移除不必要的元素,始终保持队头为窗口内的最小值对应索引。
-
滑动窗口操作:
- 每天根据队列中最小值索引的价格购买食物。
- 窗口移动时,移入当前天的价格,移出超出范围的价格。
-
复杂度分析:
- 时间复杂度:每个元素最多被加入和移除队列一次,复杂度为 ( O(N) )。
- 空间复杂度:队列中最多存储 ( K ) 个元素,复杂度为 ( O(K) )。
样例验证
样例 1:
输入:
n = 5, k = 2, data = [1, 2, 3, 3, 2]
过程:
- 第 1 天:窗口
[1],最低价格为 1,花费 1。 - 第 2 天:窗口
[1, 2],最低价格为 1,花费 1。 - 第 3 天:窗口
[2, 3],最低价格为 2,花费 2。 - 第 4 天:窗口
[3, 3],最低价格为 3,花费 3。 - 第 5 天:窗口
[3, 2],最低价格为 2,花费 2。
总花费:1 + 1 + 2 + 3 + 2 = 9。
输出:9
样例 2:
输入:
n = 6, k = 3, data = [4, 1, 5, 2, 1, 3]
输出:9
Deque 的优缺点总结
优点
- 高效地动态维护窗口内的最小值,所有操作均摊 ( O(1) )。
- 代码逻辑更语义化,操作如
pollFirst,offerLast明确表示滑动窗口的移动。
缺点
- 对于初学者,理解单调队列的维护规则可能有一定门槛。
- 适用于需要动态调整窗口内容的场景,对于固定窗口简单问题,可能略显复杂。
适用场景
Deque更适合的场景:- 动态调整窗口,或需要频繁查询窗口最值的场景(如最大值/最小值滑动窗口问题)。
- 数组更适合的场景:
- 滑动窗口固定,且只需要简单的遍历操作。
在本题中,由于需要频繁查询窗口的最小值,Deque 的使用是最优解。