做题笔记:补给站最优花费问题解析 | 豆包MarsCode AI刷题

337 阅读5分钟

做题笔记:补给站最优花费问题解析

问题描述 小U计划进行一场从地点A到地点B的徒步旅行,旅行总共需要天。为了在旅途中确保安全,小U每天都需要消耗一份食物。在路程中小U会经过一些补给站,这些补给站分布在不同的大数上,且每个补给站的食物价格各不相同。 小U需要在这些补给站中购买食物,以确保每天都有足够的食物。现在她想知道,如何规划在不同补给站的购买策略,以使她能够花费最 少的钱顺利完成这次旅行。

  • M:总路程所需的天数。
  • N:路上补给站的数量,
  • p:每个补给站的描述,包含两个数字A和 B,表示第A天有一个补给站,并且该站每份食物的价格为B元.
  • 保证第0天一定有一个补给站,并且补给站是按顺序出现的。 测试样例
  • 样例1:
    • 输入:m=5,n=4,p=[[0,2],[1,3],[2,1],[3,2]]
    • 输出:7
  • 样例2:
    • 输入:m=6,n=5,p=[[0,1],[1,5],[2,2],[3,4],[5,1]]
    • 输出:6
  • 样例3:
    • 输入:m=4,n=3,p=[[0,3],[2,2],[3,1]]
    • 输出:9

问题分析

为了求解最优购买策略,需要重点考虑以下几点:

  1. 每日消耗:每天需要消耗一份食物,意味着补给策略必须覆盖所有天数。
  2. 补给站价格差异:不同补给站的价格各不相同,必须优先考虑低价的补给站以减少总成本。
  3. 动态决策:价格最低的补给站可能需要在未来的某一天使用,必须有规划地购买。

解题思路

我设计了两种解决方法,分别基于最小堆(优先队列)和动态规划。两种方法各有特点,以下对其进行详细解读。

方法一:基于最小堆的贪心策略

核心思路
利用最小堆(PriorityQueue)动态存储当前可用的补给站价格,每一天只从堆中取出最低价格的食物进行购买。

步骤解析

  1. 初始化一个最小堆,用于维护补给站的食物价格。
  2. 遍历从第 0 天到第 (m-1) 天:
    • 如果当天有补给站,将价格加入堆中。
    • 从堆中取出价格最低的一份食物,计算总花费。
  3. 最终,堆的动态管理保证每一天的购买成本最低。

实现代码

public static int solutionHeap(int m, int n, int[][] p) {
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    int totalCost = 0;
    int index = 0; // 补给站指针

    // 遍历每一天,0 到 m-1 天
    for (int day = 0; day < m; day++) {
        // 检查当天是否有补给站,添加当天的补给站价格到堆中
        if (index < n && p[index][0] == day) {
            minHeap.offer(p[index][1]);
            index++;
        }
        // 从堆中取最低价格
        if (!minHeap.isEmpty()) {
            totalCost += minHeap.peek();
        } else {
            // 理论上不会发生,保证第0天有补给站
            return -1;
        }
    }
    return totalCost;
}

优缺点分析

  • 优点:实现简单直观,适合价格变化较大的情况。
  • 缺点:每次访问堆的复杂度为 (O(\log n)),对于补给站较多的情况效率稍低。

方法二:基于动态规划

核心思路
利用动态规划数组记录到每一天的最小花费,递推公式结合当前最优价格计算当前花费。

动态规划定义

  • 定义 (dp[i]) 为到第 (i) 天的最小总花费。
  • 状态转移方程:
    [ dp[i] = \min(dp[i-1] + \text{minPrice}, dp[i-1] + p[i][1]) ]
    其中,(\text{minPrice}) 为当前已知的最低食物价格。

实现代码

public static int solutionDP(int m, int n, int[][] p) {
    int[] dp = new int[m];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0] = p[0][1]; // 第0天的花费就是第0天补给站的价格

    int index = 1;  // 补给站位置
    int minPrice = p[0][1];  // 当前最低价格

    for (int day = 1; day < m; day++) {
        // 无论当天是否有补给站,先假设延续前一天的最优花费
        dp[day] = dp[day - 1] + minPrice; // 延续前一天的最优花费

        // 如果有,更新当前最便宜的价格
        if (index < n && p[index][0] == day) {
            minPrice = Math.min(minPrice, p[index][1]);
            dp[day] = Math.min(dp[day], dp[day - 1] + minPrice);
            index++;
        }
    }
    return dp[m - 1];
}

优缺点分析

  • 优点:适合补给站稀疏但价格变化较小的情况,数组直接存储结果,访问高效。
  • 缺点:代码复杂度较高,状态转移方程需要仔细分析。

对比与思考

  1. 时间复杂度

    • 贪心法(最小堆):(O(m \cdot \log n)),堆操作开销较高。
    • 动态规划:(O(m)),仅需线性遍历天数。
  2. 适用场景

    • 贪心法适合补给站密集、价格波动大的情况。
    • 动态规划适合补给站稀疏、价格波动较小的情况。
  3. 可扩展性

    • 动态规划能更容易扩展到其他类似问题(如每份食物重量不同)。

运行结果
两种方法在所有测试用例中均能得到正确结果。

总结与反思

  1. 问题建模:本问题是典型的动态规划和贪心策略问题,理解其核心在于“每一天最优决策如何影响未来”。
  2. 代码实现:两种方法各有优劣,需要根据场景选择。
  3. 学习收获:动态规划更加通用,但需要花费更多时间分析状态转移公式;贪心法实现简单,但在一些场景可能无法全局最优。