MarsCode算法题之补给站最优花费问题

1.问题描述

        小U计划进行一场从地点A到地点B的徒步旅行,旅行总共需要 M 天。为了在旅途中确保安全,小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

        难度等级

                简单

        题目链接

                补给站最优花费问题

2.解题思路

        这道题是最优花费问题,我们可以用动态规划的思想来解决。首先我们要确定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]返回即可。

3.代码展示

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

4.总结

        这道题的容易被忽略导致做不出的小细节就是,即使到了补给站,每一天也是要消耗时候的,容易在补给站补充的时候,遗忘了当天的消耗,也就是(j+1),而不是单纯的 j 。好了,今天这道题就啰嗦到这里,bye~

你可能感兴趣的:(豆包MarsCode算法题,算法,java,动态规划,MarsCode)