做题笔记:补给站最优花费问题解析
问题描述 小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
- 输入:
问题分析
为了求解最优购买策略,需要重点考虑以下几点:
- 每日消耗:每天需要消耗一份食物,意味着补给策略必须覆盖所有天数。
- 补给站价格差异:不同补给站的价格各不相同,必须优先考虑低价的补给站以减少总成本。
- 动态决策:价格最低的补给站可能需要在未来的某一天使用,必须有规划地购买。
解题思路
我设计了两种解决方法,分别基于最小堆(优先队列)和动态规划。两种方法各有特点,以下对其进行详细解读。
方法一:基于最小堆的贪心策略
核心思路
利用最小堆(PriorityQueue)动态存储当前可用的补给站价格,每一天只从堆中取出最低价格的食物进行购买。
步骤解析
- 初始化一个最小堆,用于维护补给站的食物价格。
- 遍历从第 0 天到第 (m-1) 天:
- 如果当天有补给站,将价格加入堆中。
- 从堆中取出价格最低的一份食物,计算总花费。
- 最终,堆的动态管理保证每一天的购买成本最低。
实现代码
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];
}
优缺点分析
- 优点:适合补给站稀疏但价格变化较小的情况,数组直接存储结果,访问高效。
- 缺点:代码复杂度较高,状态转移方程需要仔细分析。
对比与思考
-
时间复杂度
- 贪心法(最小堆):(O(m \cdot \log n)),堆操作开销较高。
- 动态规划:(O(m)),仅需线性遍历天数。
-
适用场景
- 贪心法适合补给站密集、价格波动大的情况。
- 动态规划适合补给站稀疏、价格波动较小的情况。
-
可扩展性
- 动态规划能更容易扩展到其他类似问题(如每份食物重量不同)。
运行结果
两种方法在所有测试用例中均能得到正确结果。
总结与反思
- 问题建模:本问题是典型的动态规划和贪心策略问题,理解其核心在于“每一天最优决策如何影响未来”。
- 代码实现:两种方法各有优劣,需要根据场景选择。
- 学习收获:动态规划更加通用,但需要花费更多时间分析状态转移公式;贪心法实现简单,但在一些场景可能无法全局最优。