2. 徒步旅行中的补给问题 贪心算法结合滑动窗口题解 | 豆包MarsCode AI刷题

87 阅读4分钟

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 份食物,因此无法一次性购买完成旅行所需的所有食物。

解题思路

这个问题本质上是一个具有容量限制的贪心策略问题。关键在于:

  1. 背包容量限制:小R每天的背包容量最大为 K,因此无法一次性囤积过多食物。
  2. 价格优化:由于每个补给站的价格不同,小R需要尽量选择在价格较低的补给站购买食物。
  3. 动态决策窗口:通过滑动窗口记录未来几天内可能的最低价格点,以便及时补充食物。

了解了问题的具体条件之后,我的第一反应是通过贪心算法,每次寻找后面 K 天中价格最低的那天,然后在那一天中将食物数量补充到 K 。但是在运行的过程中,存在着当前拥有的食物数量不足以到达那一天的情况,所以单单使用贪心算法无法全面地解决此问题。

每次寻找k天内的最低价格,可以使用滑动窗口的方法,因此我们可以尝试将贪心算法和滑动窗口相结合来解决此问题。

以下是解决此问题的主要思路:

  1. 滑动窗口:用双端队列deque维护一个最多包含 K 天内食物价格的滑动窗口。每次到达新补给站时,更新窗口内容,剔除不必要的价格记录。
  2. 最优选择:在每个窗口中,选取价格最低的补给站作为当前购买食物的选择。
  3. 动态更新:每天消耗1份食物,如果库存不足(小于 1 份),则需要立刻购买。

我们可以将之前的解体思路反过来,不是寻找未来第几天买 K 份,而是在之前 K 天中我需要买多少份,就相当于在这一天的前 K 天当中,我要在哪一天去买我这一天的消耗(选最小的去买),也不会有食物数量超过 K 个的问题。


图解思路

假设 n = 5, k = 2, prices = [1, 2, 3, 3, 2]

天数补给站价格Deque (索引)双端队列状态购买最低价格花费累计
11[0][1]11
22[0, 1][1, 2]12
33[1, 2][2, 3]24
43[2, 3][3, 3]37
52[3, 4][2]29

最终输出: 最小花费为 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;
}

复杂度分析

  1. 时间复杂度

    • 遍历 N 天,复杂度为 O(n)。对于每个价格,我们最多会对 deque 执行k次操作。每次插入或删除的操作时间复杂度为 O(1),而最小值查询操作也为 O(1)。因此总复杂度最好为 O(n),最差为 O(n⋅k),在 k 较小时表现良好。
  2. 空间复杂度

    • deque 中最多存储 k 个元素,因此空间复杂度为 O(k)O(k)O(k)。