字节青训营题目之最低花费问题--经典动态规划

900 阅读8分钟

字节青训营题目之最低花费问题--经典动态规划

动态规划

例题

优化青海湖至景点X的租车路线成本

徒步旅行中的补给问题

补给站最优花费问题

这三道题有类似的地方,都是求在一个地方选择获取多少补给,最终到达终点的最小花费, 只不过后两题少了一些限制,处理更容易一些,理解了第一题后后面的两题也就是小case了

首先分析第一题,也是最复杂的一题

优化青海湖至景点X的租车路线成本

小F计划从青海湖出发,前往一个遥远的景点X进行旅游。景点X可能是“敦煌”或“月牙泉”,线路的路径是唯一的。由于油价的不断上涨,小F希望尽量减少行程中的燃油成本。车辆的油箱容量为400L,在起始点租车时,车内剩余油量为 200L。每行驶 1km 消耗 1L 油。沿途设有多个加油站,小F可以在这些加油站补充燃油;此外,到达目标景点X还车的时候,需要保证车内剩余的油至少有 200L。

小F需要你帮助他计算,如果合理规划加油站的加油顺序和数量,最小化从青海湖到景点X的旅行成本(元)。

输入:

  • distance:从青海湖到景点X的总距离(km),距离最远不超过 10000 km。
  • n:沿途加油站的数量 (1 <= n <= 100)
  • gas_stations:每个加油站的信息,包含两个非负整数 [加油站距离起始点的距离(km), 该加油站的油价(元/L)]

输出:

  • 最小化从青海湖到景点X的旅行成本(元)。如果无法到达景点X,或者到达景点X还车时油料剩余不足 200L,则需要返回 -1 告诉小F这是不可能的任务。

题目要点整理

起始油量为200, 油箱容量为400

每行驶 1km 消耗 1L 油

最后要求到终点油量为200

首先我们要找到问题能否转移到子问题,也就决定了本题是否可以用DP, 也就是DP问题的关键->如何设置DP数组

如果设置dp[i][j]为经过了i个加油站, 油量为j的最小成本, 即可完成状态转移

如果不加油: dp[i+1][j-dis] = min(dp[i+1][j-dis], dp[i][j]), 成本不变. 走到下一站用了dis的油的花费和上一站的花费一样

如果加油 dp[i+1][j-dis+add] = min(dp[i+1][j-dis+add], dp[i][j]+add*pirce) 成本需要增加加油的价格

加油必须要至少到达下一个加油站, 所以add+j>=dis, 由于限制了油箱大小,所以add需要限制, j+add<=MAX_CAPACITY

最后一步就是dp初始状态的定义, 油箱状态有0到400,共401种状态, 所以dp.size()=(油站数+1, 油箱容量+1), 初始化全部为INT_MAX

初始状态 dp[0][200-dis] = 0

其他处理:

  • 由于给的加油站不一定是按照距离排列的, 所以需要先进行排列, 以保证加油站是按照距离由近到远访问的
  • 有些加油站可能比目标距离远, 这部分不需要考虑,需要删除
  • 终点需要加入一个加油站,以保证最后达到了终点
  • 注意在使用python提交时修改返回值为int类型而不是字符串, Impossible为-1
const int MAX_CAPACITY = 400;
const int INT_MAX = 1000000;
std::string solution(int distance, int n, std::vector<std::vector<int>> gasStations1) {
    // Please write your code here
    //按照距离排序
    std::sort(gasStations1.begin(),gasStations1.end(),[](vector<int>& a, vector<int>& b){return a[0]<b[0];});
    //剔除到达不了的加油站
    vector<std::vector<int>> gasStations;
    for(auto it = gasStations1.begin(); it!=gasStations1.end(); it++)
    {
        if((*it)[0]<distance)
            gasStations.push_back(*it);
        else
            break;
    }
    gasStations.push_back({distance, 0}); // 加入终点, 油价无所谓,反正不会用到
    int gas_n = gasStations.size();
    vector<vector<int>> dp(gas_n,vector<int>(MAX_CAPACITY+1,INT_MAX));
    //初始油量到达不了第一个加油站
    if(gasStations[0][0]>200)
        return "Impossible";
    //初始化dp数组
    dp[0][200-gasStations[0][0]] = 0;
    for(int i = 0; i<gas_n-1; i++)
    {
        //加油站之间的距离
        int dis = gasStations[i+1][0]-gasStations[i][0];
        int price = gasStations[i][1];
        //每种油量都要转移,可以转移到任意加油数的状态
        for(int j = 0; j<=MAX_CAPACITY; j++)
        {
            if(dp[i][j]==INT_MAX) continue;
            //此站可以选择不加油直接到达下一站
            if(j>=dis)//不加油
                dp[i+1][j-dis] = min(dp[i+1][j-dis],dp[i][j]);
            //此站选择加油
            for(int add = max(1, dis-j); add+j<=MAX_CAPACITY; add++)
            {
                dp[i+1][j+add-dis] = min(dp[i+1][j+add-dis],dp[i][j]+add*price);
            }
        }
    }
    cout<<"ans: "<<to_string(dp[gas_n-1][200])<<endl;;
    return dp[gas_n-1][200]!=INT_MAX? to_string(dp[gas_n-1][200]):"Impossible";
}

递归写法, 当前站的成本由上一站成本转移来, 由于递归dfs(i,j)的写法, 无法像DP数组那样直接对dp[i][j-dis]进行操作,所以要重新考虑转移方程,有一些些绕,但是道理都一样

不加油时:dfs(i,j) = dfs(i-1, j+dis);

加油时:dfs(i,j) = dfs(i-1, origin); 其中origin为上一站的油量, 计算方式为: origin + add - dis = j

其中加油时的转移要进行一些限制, 比如add>0,origin+add<=MAX_CAPACITY, 具体代码如下

std::string solution(int distance, int n,
                     std::vector<std::vector<int>> gasStations1) {
  // Please write your code here
  std::sort(gasStations1.begin(), gasStations1.end(),
            [](vector<int> &a, vector<int> &b) { return a[0] < b[0]; });
  //剔除到达不了的加油站
  vector<std::vector<int>> gasStations;
  for (auto it = gasStations1.begin(); it != gasStations1.end(); it++) {
    if ((*it)[0] < distance)
      gasStations.push_back(*it);
    else
      break;
  }
  gasStations.push_back({distance, 0}); // 加入终点, 油价无所谓,反正不会用到
  int gas_n = gasStations.size();
  vector<vector<int>> memo(gas_n, vector<int>(401, -1));
  //初始油量到达不了第一个加油站
  if (gasStations[0][0] > 200)
    return "Impossible";
  auto dfs = [&](auto&& dfs, int i, int j)->int
  {
    //初始状态只有油量为200-gasStations[0][0]的状态
    if(i==0)
      return j==200-gasStations[0][0]?0:INT_MAX;
    int& res = memo[i][j];
    if(res != -1)
      return res;
    int dis = gasStations[i][0] - gasStations[i-1][0];
    int price = gasStations[i-1][1];
    res = INT_MAX;
    //不加油, 此时的状态可由不加油的状态转移过来
    if (j+dis<=MAX_CAPACITY)
    {
      res = min(res, dfs(dfs, i-1, j+dis));
    }
    //加油了
    //遍历原始油量 origin + add - dis = j 原始油量+加油量-消耗油量=下一站的剩余油量
    for(int origin = 0; origin<MAX_CAPACITY; origin++)
    {
        int add = j-origin+dis;
        //保证加油的量>0且加油之后不会超过容量
        if(add>0&&add+origin<=MAX_CAPACITY)
          res = min(res, dfs(dfs, i-1, origin)+add * price);
    }
    return res;
  };
  int ans = dfs(dfs, gas_n-1, 200);
  cout<<"ans: "<<ans<<endl;
  return ans == INT_MAX? "Impossible" : to_string(ans);

徒步旅行中的补给问题

小R正在计划一次从地点A到地点B的徒步旅行,总路程需要 N 天。为了在旅途中保持充足的能量,小R每天必须消耗1份食物。幸运的是,小R在路途中每天都会经过一个补给站,可以购买食物进行补充。然而,每个补给站的食物每份的价格可能不同,并且小R最多只能同时携带 K 份食物。

现在,小R希望在保证每天都有食物的前提下,以最小的花费完成这次徒步旅行。你能帮助小R计算出最低的花费是多少吗?

解答:

理解了第一题后这道题也就迎刃而解了, 这一题同样规定了最多只能携带K个食物, 但是补给点为每一天一个,与上一题类似, 只不过省去了距离dis的判断

int solution(int n, int k, std::vector<int> data) {
    vector<vector<int>> memo(n, vector<int>(k+1, -1));
    auto dfs = [&](auto&& dfs, int i, int bag) -> int 
    {
        if (i == n) {
            return 0;
        }

        int& res = memo[i][bag];
        if (res!=-1) {
            return res;
        }

        int ans = 100000;
        // 决定买多少份食物
        int j = (bag == 0) ? 1 : 0; // bag == 0 时必须买
        for (; j <= k - bag; ++j) {
            int current_cost = j * data[i] + dfs(dfs, i + 1, bag + j - 1);
            ans = min(ans, current_cost);
        }

        memo[i][bag] = ans;
        return ans;
    };

    return dfs(dfs, 0, 0);
}

补给站最优花费问题

小U计划进行一场从地点A到地点B的徒步旅行,旅行总共需要 M 天。为了在旅途中确保安全,小U每天都需要消耗一份食物。在路程中,小U会经过一些补给站,这些补给站分布在不同的天数上,且每个补给站的食物价格各不相同。

小U需要在这些补给站中购买食物,以确保每天都有足够的食物。现在她想知道,如何规划在不同补给站的购买策略,以使她能够花费最少的钱顺利完成这次旅行。

  • M:总路程所需的天数。
  • N:路上补给站的数量。
  • p:每个补给站的描述,包含两个数字 A 和 B,表示第 A 天有一个补给站,并且该站每份食物的价格为 B 元。

保证第0天一定有一个补给站,并且补给站是按顺序出现的。

解:

这一题同样类似第一题,只不过没有了最大容量MAX_CAPACITY的限制

int solution(int m, int n, std::vector<std::vector<int>> p) {
    int food_index = 0;
    p.push_back({m, 0});
    std::vector<std::vector<int>> memo(n, std::vector<int>(m,-1));
    auto dfs = [&](auto&& dfs, int i, int bag)->int
    {
        if(i==n)
            return 0;
        int& res = memo[i][bag];
        if(res != -1)
            return res;
        int price = p[i][1];
        int dis = p[i+1][0] - p[i][0];
        int temp = 10000;
        //不买
        if(bag>=dis)
            temp = std::min(temp, dfs(dfs, i+1, bag-dis));
        //买
        for(int buy = 1; buy<=m-p[i][0]; buy++)
        {
            if(buy + bag >= dis)
                temp = std::min(temp, buy*price+dfs(dfs, i+1, bag+buy-dis));
        }
        res = temp;
        return temp;
    };
    int ans = dfs(dfs, 0, 0);
    return dfs(dfs, 0, 0);
}

第一次写题解,有写的不对的的地方大家多多包涵, 只是分享一下自己的解题思路,也想借着这个机会结识更多的志同道合的小伙伴!