2.徒步旅行中的补给问题
问题描述
小R正在计划一次从地点A到地点B的徒步旅行,总路程需要 N 天。为了在旅途中保持充足的能量,小R每天必须消耗1份食物。幸运的是,小R在路途中每天都会经过一个补给站,可以购买食物进行补充。然而,每个补给站的食物每份的价格可能不同,并且小R最多只能同时携带 K 份食物。
现在,小R希望在保证每天都有食物的前提下,以最小的花费完成这次徒步旅行。你能帮助小R计算出最低的花费是多少吗?
测试样例
样例1:
输入:
n = 5 ,k = 2 ,data = [1, 2, 3, 3, 2]
输出:9
样例2:
输入:
n = 6 ,k = 3 ,data = [4, 1, 5, 2, 1, 3]
输出:9
样例3:
输入:
n = 4 ,k = 1 ,data = [3, 2, 4, 1]
输出:10
问题解析
题目解析
-
每天必须消耗 1 份食物。
-
每个补给站提供的食物单价不同。
-
小R最多能携带 K 份食物,因此无法一次性购买完成旅行所需的所有食物。
解题思路
这个问题本质上是一个具有容量限制的贪心策略问题。关键在于:
- 背包容量限制:小R每天的背包容量最大为 K,因此无法一次性囤积过多食物。
- 价格优化:由于每个补给站的价格不同,小R需要尽量选择在价格较低的补给站购买食物。
- 动态决策窗口:通过滑动窗口记录未来几天内可能的最低价格点,以便及时补充食物。
了解了问题的具体条件之后,我的第一反应是通过贪心算法,每次寻找后面 K 天中价格最低的那天,然后在那一天中将食物数量补充到 K 。但是在运行的过程中,存在着当前拥有的食物数量不足以到达那一天的情况,所以单单使用贪心算法无法全面地解决此问题。
每次寻找k天内的最低价格,可以使用滑动窗口的方法,因此我们可以尝试将贪心算法和滑动窗口相结合来解决此问题。
以下是解决此问题的主要思路:
- 滑动窗口:用双端队列
deque维护一个最多包含 K 天内食物价格的滑动窗口。每次到达新补给站时,更新窗口内容,剔除不必要的价格记录。 - 最优选择:在每个窗口中,选取价格最低的补给站作为当前购买食物的选择。
- 动态更新:每天消耗1份食物,如果库存不足(小于 1 份),则需要立刻购买。
我们可以将之前的解体思路反过来,不是寻找未来第几天买 K 份,而是在之前 K 天中我需要买多少份,就相当于在这一天的前 K 天当中,我要在哪一天去买我这一天的消耗(选最小的去买),也不会有食物数量超过 K 个的问题。
图解思路
假设 n = 5, k = 2, prices = [1, 2, 3, 3, 2]。
| 天数 | 补给站价格 | Deque (索引) | 双端队列状态 | 购买最低价格 | 花费累计 |
|---|---|---|---|---|---|
| 1 | 1 | [0] | [1] | 1 | 1 |
| 2 | 2 | [0, 1] | [1, 2] | 1 | 2 |
| 3 | 3 | [1, 2] | [2, 3] | 2 | 4 |
| 4 | 3 | [2, 3] | [3, 3] | 3 | 7 |
| 5 | 2 | [3, 4] | [2] | 2 | 9 |
最终输出: 最小花费为 9。
代码实现
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
// 求解函数
int solution(int n, int k, vector<int> data) {
int min_money = 0; // 总花费
deque<int> window; // 双端队列,维护窗口中最小值的索引
for (int i = 0; i < n; ++i) {
// 移除过期的元素,窗口超出范围的元素
while (!window.empty() && window.front() <= i - k) {
window.pop_front();
}
// 在队列中移除所有比当前值大的元素,因为它们不会再用到了
while (!window.empty() && data[window.back()] > data[i]) {
window.pop_back();
}
// 将当前元素的索引添加到队列中
window.push_back(i);
// 当前窗口的最小值在队列的前面
min_money += data[window.front()];
}
return min_money;
}
int main() {
// 测试样例
cout << (solution(5, 2, {1, 2, 3, 3, 2})) << endl; // 输出 9
cout << (solution(6, 3, {4, 1, 5, 2, 1, 3})) << endl; // 输出 9
cout << (solution(4, 1, {3, 2, 4, 1})) << endl; // 输出 10
return 0;
}
复杂度分析
-
时间复杂度:
- 遍历 N 天,复杂度为 O(n)。对于每个价格,我们最多会对
deque执行k次操作。每次插入或删除的操作时间复杂度为 O(1),而最小值查询操作也为 O(1)。因此总复杂度最好为 O(n),最差为 O(n⋅k),在 k 较小时表现良好。
- 遍历 N 天,复杂度为 O(n)。对于每个价格,我们最多会对
-
空间复杂度:
deque中最多存储k个元素,因此空间复杂度为 O(k)O(k)O(k)。