徒步旅行中的补给问题

Jan 18

试题分析

题目描述:

小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. 每天必须消耗 1 份食物,如果背包中食物不足,需要从价格最低的补给站购买。
  2. 背包最多能携带 k 份食物,因此不能过量购买。
  3. 如何高效找到当前窗口内(即最近 k 天)的最小价格?

解决方案

使用 单调队列(Deque) 来维护滑动窗口的最小值:

  1. 队列维护窗口的最小值:
    • 队列的每个元素存储当前窗口的价格和对应索引。
    • 队列中的值从队尾到队首单调递增,队首元素始终是窗口的最小值。
  2. 动态维护窗口:
    • 每次加入新元素时,从队尾移除所有比当前价格大的元素。
    • 每次滑动窗口时,移除队首超出窗口范围的元素。
  3. 高效累加结果:
    • 每天累加窗口的最小值,即队首的价格。

复杂度分析

  1. 时间复杂度:每个元素最多被插入和移除队列一次,因此总时间复杂度为O(n)
  2. 空间复杂度:双端队列最多存储 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天的价格。我们用一个队列来存储当前窗口的价格,按从小到大的顺序排列。直接看每天发生了什么。

天数当前价格窗口范围队列状态(存储价格)最小值累加结果
11[1][1]11
22[1, 2][1, 2]12
33[2, 3][2, 3]24
43[3, 3][3, 3]37
52[3, 2][2]29

通过观察每天的变化,我们明确以下核心任务:

  1. 维护一个滑动窗口:窗口大小为 k ,表示小R最多能携带的食物天数。
  2. 在窗口中找到价格最低的补给站:从窗口中选择最小值,代表当天的最低花费。
  3. 动态更新窗口和最小值:
    • 移除过期的值(不在窗口范围内)。
    • 移除比当前值更大的元素,确保队列能快速获取最小值。
步骤具体操作代码逻辑
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 ) 的最短长度。
CC BY-NC-SA 4.0 2021-PRESENT © Jaguar Liu