动态规划:优化青海湖至景点X的租车路线成本

110 阅读9分钟

动态规划:优化青海湖至景点X的租车路线成本

Created: 2024年11月21日 16:17 Class: 动态规划

优化青海湖至景点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这是不可能的任务。

测试样例

样例1:

输入:distance = 500, n = 4, gas_stations = [[100, 1], [200, 30], [400, 40], [300, 20]]

输出:4300

样例2:

输入:distance = 1000, n = 3, gas_stations = [[300, 25], [600, 35], [900, 5]]

输出:-1

样例3:

输入:distance = 200, n = 2, gas_stations = [[100, 50], [150, 45]]

输出:9000

样例4:

输入:distance = 700, n = 5, gas_stations = [[100, 10], [200, 20], [300, 30], [400, 40], [600, 15]]

输出:9500

样例5:

输入:distance = 50, n = 1, gas_stations = [[25, 100]]

输出:5000

思路

我学动态规划啦🥳🥳🥳 哈——哈——哈——!

我做动态规划,是先用二维数组推导,写代码的时候用一维数组写。

初始思考

推导前,先思考几个问题:

  1. dp二维数组的值设置成什么?

    :最低价格

  2. dp二维数组的横纵坐标分别表示什么?

    :行:用gas_stations节点遍历;

    :列:距离限制;

  3. 求解顺序是什么?

    :从左到右,从上到下。外层用gas_stations节点,内层用距离。

什么意思?举个例子,样例1.(一些细节后续再说,只是先看一下形式):

01200300400500600700
[100, 1]
[200, 30]
[300, 20]
[400, 40]

大概是这个样子。光靠在脑子里凭空演算还是有点麻烦的,在纸上推导一下清晰且快速。

细节分析

先用二维数组分析哈,至于写代码的时候用一维数组去优化一下代码即可。

完全背包的最低值

因为距离是+1的,所以这个节点加油是可以反复利用的,虽然有一个400L的限制,但是没关系,我们可以手动设置更新范围嘛,所以这还是一个属于完全背包的问题。

完全背包问题,求最低值的通用套路模板:

相关题目:322. 零钱兑换

class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[amount + 1];
        //初始化dp数组为最大值
        for (int j = 0; j < dp.length; j++) {
            dp[j] = max;
        }
        //当金额为0时需要的硬币数目为0
        dp[0] = 0;
        for (int i = 0; i < coins.length; i++) {
            //正序遍历:完全背包每个硬币可以选择多次
            for (int j = coins[i]; j <= amount; j++) {
                //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                if (dp[j - coins[i]] != max) {
                    //选择硬币数目最小的情况
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        return dp[amount] == max ? -1 : dp[amount];
    }
}

几个注意点:

  • 初始化设置为 Integer.MAX_VALUE
  • 判断:不是初始最大值时,该位才有选择的必要 dp[j - coins[i]] != max
  • 最小值:dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);

距离设置

要求到达终点的时候,需要保证车内剩余的油至少有 200L。转换一下思路,不就是刚刚好到达 distance+200 最少要花多少钱嘛!

所以dp数组的维度应该是 int[gas_stations.size()][distance+200+1] .

初始化数组

前200KM,不用花钱,所以[0,200]都是为0的。其余的就设置成 Integer.MAX_VALUE

更新数组

好,更新这个数组就是先从第一个节点开始,从左到右依次更新,一行结束后,再用第二个节点更新,以此类推,最右下角的值就是最终结果。

那我们回过头来想想,每个数组的值在推导的时候代表着什么含义?最低价格,什么的最低价格?

:想要到达这个距离,选择在该节点加油或不加油,需要花费的最小金额。

记住这个目的。我们来更新数组:

因为有了油箱容量为400L的限制,所以,从可以到这节点的距离开始,一直到这个距离+400,是这个节点的更新范围。因为数组里的数值都是刚刚好到达,所以没有油的剩余,也就是说我的油箱为空的时候,在这个节点加油,最远能到达哪里?当然是这个节点往后的400KM啦。

好!第一个节点:[100, 1],更新的范围是[101, 500]

  • 101-200这个区间,不用加油,本身就能到达。也就是最小值就是我们初始化设置的0.
  • 201-500,是必须要加油的。怎么加?
01200300400500600700
INIT000MMMMM
[100, 1]000100200300MM
[200, 30]
[300, 20]
[400, 40]

第二个节点:[200, 30],更新范围:[201, 600]

  • 201-500,需要跟上面一行的值比较。举个例子,400这个点,想要到400,需要399加一公里,一公里花的钱,跟我之前的花的相比,哪个更少。当然还是第一个节点便宜,所以不用动。
  • 501-600,第一个节点到不了,只能用第二个节点更新啦
01200300400500501600700
INIT000MMMMMM
[100, 1]000100200300MMM
[200, 30]0001002003003303300M
[300, 20]
[400, 40]

第三个节点:[300, 20],更新范围:[301, 700]

  • 301-500,仍然比不过节点1
  • 501-600,以501举例,第三个节点的花销应该是,500的花销+20,也就是320,而前面我们推导出来,是330,所以将这个值更新为320.
01200300400500501600700
INIT000MMMMMM
[100, 1]000100200300MMM
[200, 30]0001002003003303300M
[300, 20]00010020030032023004300
[400, 40]

第四个节点:[400, 40],更新范围:[401, 700],最远就到700,哪来的800呢。

没有截止到第三个节点的划算,所以不更新。

01200300400500501600700
INIT000MMMMMM
[100, 1]000100200300MMM
[200, 30]0001002003003303300M
[300, 20]00010020030032023004300
[400, 40]00010020030032023004300

那我们可以把递推公式写出来了:

:前一公里的价钱+该节点的1一公里的价钱,和之前的值比较,取最小值:

dp[i] = Math.min(dp[i - 1] + item.get(1), dp[i]);

其他细节

因为题目的gas_stations是乱序的,所以需要先按照距离起始点的距离排一下序。

然后我们就可以提前判断一下

  • 初始节点的距离>200,那必然到不了
  • 压根没有节点,补不了油,也到不了。

通畅了!写代码!

代码

import java.util.*;

public class Main {
    public static int solution(int distance, int n, List<List<Integer>> gas_stations) {
        // Please write your code here
        // 排序
        gas_stations.sort(Comparator.comparingInt(a -> a.get(0)));
        if (gas_stations.size() == 0 ||
            gas_stations.get(0).get(0) > 200) {
            return -1;
        }

        int len = distance + 200 + 1;
        int max = Integer.MAX_VALUE;
        int[] dp = new int[len];
        for (int i = 201; i < len; i++) {
            dp[i] = max;
        }

        for (List<Integer> item : gas_stations) {
            for (int i = item.get(0) + 1; i <= Math.min((item.get(0) + 400), len - 1); i++) {
                if (dp[i - 1] != max) {
                    dp[i] = Math.min(dp[i - 1] + item.get(1), dp[i]);
                }
            }
        }
        if (dp[len - 1] == max) {
            return -1;
        }
        return dp[len - 1];
    }

    public static void main(String[] args) {
        List<List<Integer>> gasStations1 = new ArrayList<>();
        gasStations1.add(List.of(100, 1));
        gasStations1.add(List.of(200, 30));
        gasStations1.add(List.of(400, 40));
        gasStations1.add(List.of(300, 20));

        List<List<Integer>> gasStations2 = new ArrayList<>();
        gasStations2.add(List.of(100, 999));
        gasStations2.add(List.of(150, 888));
        gasStations2.add(List.of(200, 777));
        gasStations2.add(List.of(300, 999));
        gasStations2.add(List.of(400, 1009));
        gasStations2.add(List.of(450, 1019));
        gasStations2.add(List.of(500, 1399));

        // List<List<Integer>> gasStations3 = new ArrayList<>();
        // gasStations3.add(List.of(101));
        // gasStations3.add(List.of(100, 100));
        // gasStations3.add(List.of(102, 1));

        List<List<Integer>> gasStations4 = new ArrayList<>();
        gasStations4.add(List.of(34, 1));
        gasStations4.add(List.of(105, 9));
        gasStations4.add(List.of(9, 10));
        gasStations4.add(List.of(134, 66));
        gasStations4.add(List.of(215, 90));
        gasStations4.add(List.of(999, 1999));
        gasStations4.add(List.of(49, 0));
        gasStations4.add(List.of(10, 1999));
        gasStations4.add(List.of(200, 2));
        gasStations4.add(List.of(300, 500));
        gasStations4.add(List.of(12, 34));
        gasStations4.add(List.of(1, 23));
        gasStations4.add(List.of(46, 20));
        gasStations4.add(List.of(80, 12));
        gasStations4.add(List.of(1, 1999));
        gasStations4.add(List.of(90, 33));
        gasStations4.add(List.of(101, 23));
        gasStations4.add(List.of(34, 88));
        gasStations4.add(List.of(103, 0));
        gasStations4.add(List.of(1, 1));

        List<List<Integer>> gasStations5 = new ArrayList<>();
        gasStations1.add(List.of(25, 100));

        System.out.println(solution(500, 4, gasStations1) == 4300);
        System.out.println(solution(500, 7, gasStations2) == 410700);
        // System.out.println(solution(500, 3, gasStations3) == -1);
        System.out.println(solution(100, 20, gasStations4) == 0);
        System.out.println(solution(100, 0, new ArrayList<>()) == -1);
        System.out.println(solution(50, 1, gasStations5) == -1);
    }
}

总结

动态规划真好用,之前做绿洲之旅那道题,还没有学到动态规划,还在用单调队列解决问题,然后刷到这道题,一脸懵逼,怎么如此复杂,就先搁置了。现在回过头看看,用背包的思想真的很简单。刷题!刷题!还差图论和回溯,就基本上学完啦,加油!