试题分析 #
题目描述:
小R计划一次从地点 A 到地点 B 的徒步旅行,总路程需要 n
天。每天需要消耗 1 份食物,并可以在每天经过的补给站购买食物。补给站的食物价格每天可能不同,小R最多可以同时携带 k
份食物。
目标是:在保证每天都能消耗 1 份食物的前提下,完成徒步旅行的最小花费。
输入:
n
:旅行总天数。k
:背包容量,即最多携带的食物份数。text{data[i]}
:第 i 天补给站每份食物的价格。
输出:
- 最小花费。
约束条件:
1 < n, k < 1000
1 < \text{data[i]} < 10,000
解题思路 #
本题的关键是找到每个窗口内的最小价格,并在满足背包容量限制的情况下尽量从价格最低的补给站购买食物。这是一个典型的滑动窗口最小值问题。
核心问题
- 每天必须消耗 1 份食物,如果背包中食物不足,需要从价格最低的补给站购买。
- 背包最多能携带
k
份食物,因此不能过量购买。 - 如何高效找到当前窗口内(即最近
k
天)的最小价格?
解决方案
使用 单调队列(Deque) 来维护滑动窗口的最小值:
- 队列维护窗口的最小值:
- 队列的每个元素存储当前窗口的价格和对应索引。
- 队列中的值从队尾到队首单调递增,队首元素始终是窗口的最小值。
- 动态维护窗口:
- 每次加入新元素时,从队尾移除所有比当前价格大的元素。
- 每次滑动窗口时,移除队首超出窗口范围的元素。
- 高效累加结果:
- 每天累加窗口的最小值,即队首的价格。
复杂度分析 #
- 时间复杂度:每个元素最多被插入和移除队列一次,因此总时间复杂度为
O(n)
。 - 空间复杂度:双端队列最多存储
k
个元素,因此空间复杂度为O(k)
。
代码实现 #
以下是基于 Java 的实现代码:
import java.util.Deque;
import java.util.LinkedList;
public class Main {
public static int solution(int n, int k, int[] data) {
// 检查输入有效性
if (data.length != n || k >= n) {
throw new IllegalArgumentException("Invalid input");
}
// 双端队列存储索引和对应的值,保证队首为窗口的最小值
Deque<int[]> mins = new LinkedList<>();
int result = 0;
for (int j = 0; j < n; j++) {
// 维护双端队列,确保从队尾到队首值递增
while (!mins.isEmpty() && mins.peekLast()[1] > data[j]) {
mins.pollLast();
}
mins.addLast(new int[]{j, data[j]});
// 移除窗口外的元素
while (!mins.isEmpty() && mins.peekFirst()[0] <= j - k) {
mins.pollFirst();
}
// 当前窗口的最小值累加到结果中
result += mins.peekFirst()[1];
}
return result;
}
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
}
}
代码解析 #
假设 n = 5, k = 2, text{data} = [1, 2, 3, 3, 2]
。我们维护一个窗口大小为 k
的滑动窗口,每次只考虑最近 k
天的价格。我们用一个队列来存储当前窗口的价格,按从小到大的顺序排列。直接看每天发生了什么。
天数 | 当前价格 | 窗口范围 | 队列状态(存储价格) | 最小值 | 累加结果 |
---|---|---|---|---|---|
1 | 1 | [1] | [1] | 1 | 1 |
2 | 2 | [1, 2] | [1, 2] | 1 | 2 |
3 | 3 | [2, 3] | [2, 3] | 2 | 4 |
4 | 3 | [3, 3] | [3, 3] | 3 | 7 |
5 | 2 | [3, 2] | [2] | 2 | 9 |
通过观察每天的变化,我们明确以下核心任务:
- 维护一个滑动窗口:窗口大小为
k
,表示小R最多能携带的食物天数。 - 在窗口中找到价格最低的补给站:从窗口中选择最小值,代表当天的最低花费。
- 动态更新窗口和最小值:
- 移除过期的值(不在窗口范围内)。
- 移除比当前值更大的元素,确保队列能快速获取最小值。
步骤 | 具体操作 | 代码逻辑 |
---|---|---|
1. 加入新价格 | 将当天的价格加入队列,同时移除队列中比当前价格大的值,确保队列递增。 | while (!deque.isEmpty() && data[deque.peekLast()] > data[j]) deque.pollLast(); |
2. 移除过期价格 | 如果队列中的最小值(队首)已经超出窗口范围,则将其移除。 | if (!deque.isEmpty() && deque.peekFirst() < j - k + 1) deque.pollFirst(); |
3. 获取最小值 | 当前窗口的最小值为队列的队首值,表示当前价格范围内的最低花费。 | result += data[deque.peekFirst()]; |
4. 滑动窗口 | 窗口向右滑动一天,将索引 ( j ) 增加,并重复以上步骤。 | for (int j = 0; j < n; j++) |
5. 累加结果 | 累加每一天窗口的最小值到总结果,最终返回所有天数的最小花费。 | return result; |
关联题目 #
题目编号 | 题目名称 | 链接 | 相似点 | 不同点 |
---|---|---|---|---|
239 | 滑动窗口最大值 (Sliding Window Maximum) | 点击查看 | 滑动窗口+单调队列,维护窗口最优值。 | 本题求最小值,239 求最大值。 |
862 | 和至少为 K 的最短子数组 | 点击查看 | 动态窗口调整,满足特定条件时更新结果。 | 本题窗口内维护最优价格,862 求和满足 ( K ) 的最短子数组长度。 |
1425 | 带限制的子序列和 (Constrained Subsequence Sum) | 点击查看 | 滑动窗口限制为 ( k ),单调队列维护窗口内的最优值。 | 本题关注最小值,1425 求最大子序列和。 |
1004 | 最大连续 1 的个数 III | 点击查看 | 滑动窗口问题,动态调整窗口范围以满足约束条件。 | 本题维护价格最优,1004 关注最大连续 1 的长度。 |
1499 | 满足不等式的最大值 | 点击查看 | 滑动窗口 + 单调队列,限制窗口大小,动态更新窗口最优值。 | 本题维护价格最小值,1499 是关于点的最大值计算。 |
727 | 最小窗口子序列 | 点击查看 | 滑动窗口调整范围,满足特定条件时记录窗口的结果。 | 本题关注最小价格,727 关注子序列匹配问题。 |
209 | 长度最小的子数组 | 点击查看 | 滑动窗口动态调整,满足条件时更新结果。 | 本题关注价格最优,209 求满足和 ( \geq S ) 的最短长度。 |