小U计划进行一场从地点A到地点B的徒步旅行,旅行总共需要 M
天。为了在旅途中确保安全,小U每天都需要消耗一份食物。在路程中,小U会经过一些补给站,这些补给站分布在不同的天数上,且每个补给站的食物价格各不相同。
小U需要在这些补给站中购买食物,以确保每天都有足够的食物。现在她想知道,如何规划在不同补给站的购买策略,以使她能够花费最少的钱顺利完成这次旅行。
M
:总路程所需的天数。N
:路上补给站的数量。p
:每个补给站的描述,包含两个数字 A
和 B
,表示第 A
天有一个补给站,并且该站每份食物的价格为 B
元。保证第0天一定有一个补给站,并且补给站是按顺序出现的。
输入:
m = 5 ,n = 4 ,p = [[0, 2], [1, 3], [2, 1], [3, 2]]
输出:7
输入:
m = 6 ,n = 5 ,p = [[0, 1], [1, 5], [2, 2], [3, 4], [5, 1]]
输出:6
输入:
m = 4 ,n = 3 ,p = [[0, 3], [2, 2], [3, 1]]
输出:9
简单
补给站最优花费问题
这道题是最优花费问题,我们可以用动态规划的思想来解决。首先我们要确定dp数组的含义。定义一个dp[m+1][m+1]的数组,用dp数组来表示第i天结束时剩余j份补给所花费的时间。
//dp数组的含义:第i天结束时剩余j份补给所花费的价钱
int[][] dp = new int[m+1][m+1];
确定完dp数组的含义时候,我们就要来分析得出它的递推公式。根据题意我们可以知道,并不是每一天都有补给站可以补给食物的。因此我们的递推公式要分为两种情况,第一种是不补给的情况,第二种是有补给站要补给的情况。
我们想来看看不补给的情况,如果我们不补给的话,当天会消耗掉一份食物,假设我们当天还剩下j份食物,那所花费的价钱就和前一天还剩下(j+1)份食物一样,+1的这一份就是我们当前消耗掉的。所以我们就可以得出不补给的递推公式:dp[i][j] = dp[i-1][j+1]。
//不补给的情况
dp[i][j] = dp[i-1][j+1];
再讲补给的情况之前,因为补给站不是每天都有,所以我们要定义一个指针变量(index),来指向下一次我们会遇到的补给站。如果p[index][0] == i ,说明今天有补给站。遇到补给站之后,还要记得在当天结束的时候,让指针指向下一个补给站。
if(index < p.length && p[index][0] == i)
......
//如果今天有补给站,补给站指针移动
if(index < p.length && p[index][0] == i){
index++;
}
接下来,我们来看看遇到补给站补给的情况,如果我们遇到补给站补给,我们就要考虑我们补给了多少份食物,假设补给完,我们剩下j份食物,那我们最多也是补给j份食物,我们可以假设一个变量k来表示补给了的时候。那么,当天剩下j份食物,其中k份是补给的,花费是p[index][1]*k,再加上要消耗掉一份,所以当天剩下j份食物的花费为上一天剩下(j - k + 1)份食物的花费加上当天补给的花费,即dp[i][j] = dp[i-1][j - k + 1] + p[index][1] * k。又因为我们是要寻找最少的花费,所以我们会遍历k的所有情况,我们要在k的所有情况中找到最小的花费,因此我们要在上一个k的花费dp[i][j] 和 当天k的花费dp[i-1][j - k + 1] + p[index][1] * k中取最小值。综上所诉,我们的递归公式合并起来应该为dp[i][j] = Math.min(dp[i][j],dp[i-1][j - k + 1] + p[index][1] * k)。
要注意的是,我上一步提到的dp[i][j]在遍历买多少个之前的值,是不补给的情况的值,使用不补给的情况来和补给的情况取最小花费。
//有补给站,购买k份食物
for(int k = 0;k <= j;k++){
dp[i][j] = Math.min(dp[i][j],dp[i-1][j - k + 1] + k * p[index][1]);
}
确定完递推公式之后,我们就可以来初始化dp数组了。dp数组的初始化,就是第0天出发时,在补给站购买补给的花费,因为没有限制最多可以携带多少补给,所以我们要m份补给全部初始化。
//初始化dp数组
for(int i = 0;i <= m;i++){
dp[0][i] = p[index][1] * i;
}
index++;
初始化完dp数组之后,我们只需开始遍历,用递推公式递推就好了。在遍历的时候,有一个小优化,那就是携带的时候如果已经足够满足我们剩下的天数(m - i)的需要了,那我们就不需要在补给了,直接跳过补给站那部分。
//剩余的食物足够走完了
if(j >= m - i){
dp[i][j] = dp[i - 1][j+1];
}
最后,最优花费当然就是补给刚刚好支撑我们到达目的地的情况,也就是dp[m][0],所以我们将dp[m][0]返回即可。
public class Main {
public static int solution(int m, int n, int[][] p) {
// Edit your code here
//dp数组的含义:第i天结束时剩余j份补给所花费的价钱
int[][] dp = new int[m+1][m+1];
//标记下一个补给站的索引
int index = 0;
//初始化dp数组
for(int i = 0;i <= m;i++){
dp[0][i] = p[index][1] * i;
}
index++;
//第i天
for(int i = 1;i <= m;i++){
//剩余j份食物
for(int j = 0;j < m;j++){
//剩余的食物足够走完了
if(j >= i -m){
dp[i][j] = dp[i - 1][j+1];
}
//不补给的情况
dp[i][j] = dp[i-1][j+1];
//判断今天是否到达补给站
if(index < p.length && p[index][0] == i){
//有补给站,购买k份食物
for(int k = 0;k <= j;k++){
dp[i][j] = Math.min(dp[i][j],dp[i-1][j - k + 1] + k * p[index][1]);
}
}
}
//如果今天有补给站,补给站指针移动
if(index < p.length && p[index][0] == i){
index++;
}
}
return dp[m][0];
}
public static void main(String[] args) {
// Add your test cases here
System.out.println(solution(5, 4, new int[][]{{0, 2}, {1, 3}, {2, 1}, {3, 2}}) == 7);
}
}
这道题的容易被忽略导致做不出的小细节就是,即使到了补给站,每一天也是要消耗时候的,容易在补给站补充的时候,遗忘了当天的消耗,也就是(j+1),而不是单纯的 j 。好了,今天这道题就啰嗦到这里,bye~