徒步旅行中的补给问题

48 阅读4分钟

题目描述

小R计划一次从地点 A 到地点 B 的徒步旅行,总路程需要 ( N ) 天。为了在路途中保持充足的能量,小R每天必须消耗 1 份食物。幸运的是,小R每天会经过一个补给站,可以购买食物进行补充。然而:

  1. 每个补给站的食物价格不同。
  2. 小R最多只能同时携带 ( K ) 份食物。

目标是计算完成这次旅程所需的最低花费。


问题分析

  1. 核心约束

    • 每天必须消耗 1 份食物,不能出现断粮的情况。
    • 小R最多只能携带 ( K ) 份食物,因此需要规划补充食物的时间和数量,以尽量利用低价的补给站。
  2. 优化目标

    • 在保证每天有食物的前提下,使得总花费最小。

解题思路

我们需要维护一个滑动窗口,表示未来 ( K ) 天内小R可以选择的补给站价格范围。每次在窗口中选择最低价格的补给站进行补充,确保最低花费。

滑动窗口的操作

  1. 窗口更新
    每天移动窗口,移入当前天的价格,移出超过窗口范围的价格。

  2. 最小值查询
    每天从窗口中选择价格最低的补给站进行购买。

Deque 的作用

Deque 是 Java 中的双端队列,支持高效的头尾操作,特别适合动态维护窗口中的最小值:

  • 保持队列中的元素单调递增(队头为最小值)。
  • 移入新元素时,移除队列尾部所有比当前价格高的元素,保证队列单调性。
  • 窗口超出范围时,从队头移除过期元素。

算法步骤

  1. 初始化一个双端队列 Deque,用于存储窗口中的有效价格索引。

  2. 遍历每一天的价格:

    • 移除队头过期的元素(超出窗口范围)。
    • 移除队尾所有大于当前价格的元素,保持队列单调性。
    • 将当前价格的索引加入队尾。
    • 取队头对应的价格作为当天的购买价格,并累加到总花费中。
  3. 最终输出总花费。


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
    }
}

代码逻辑详解

  1. 维护窗口的最小值

    • 单调队列通过移除不必要的元素,始终保持队头为窗口内的最小值对应索引。
  2. 滑动窗口操作

    • 每天根据队列中最小值索引的价格购买食物。
    • 窗口移动时,移入当前天的价格,移出超出范围的价格。
  3. 复杂度分析

    • 时间复杂度:每个元素最多被加入和移除队列一次,复杂度为 ( 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 的优缺点总结

优点

  1. 高效地动态维护窗口内的最小值,所有操作均摊 ( O(1) )。
  2. 代码逻辑更语义化,操作如 pollFirst, offerLast 明确表示滑动窗口的移动。

缺点

  1. 对于初学者,理解单调队列的维护规则可能有一定门槛。
  2. 适用于需要动态调整窗口内容的场景,对于固定窗口简单问题,可能略显复杂。

适用场景

  • Deque 更适合的场景
    • 动态调整窗口,或需要频繁查询窗口最值的场景(如最大值/最小值滑动窗口问题)。
  • 数组更适合的场景
    • 滑动窗口固定,且只需要简单的遍历操作。

在本题中,由于需要频繁查询窗口的最小值,Deque 的使用是最优解。