动态规划:优化青海湖至景点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
思路
我学动态规划啦🥳🥳🥳 哈——哈——哈——!
我做动态规划,是先用二维数组推导,写代码的时候用一维数组写。
初始思考
推导前,先思考几个问题:
-
dp二维数组的值设置成什么?
:最低价格
-
dp二维数组的横纵坐标分别表示什么?
:行:用gas_stations节点遍历;
:列:距离限制;
-
求解顺序是什么?
:从左到右,从上到下。外层用gas_stations节点,内层用距离。
什么意思?举个例子,样例1.(一些细节后续再说,只是先看一下形式):
| 0 | 1 | … | 200 | … | 300 | … | 400 | … | 500 | … | 600 | … | 700 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| [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,是必须要加油的。怎么加?
| 0 | 1 | … | 200 | … | 300 | … | 400 | … | 500 | … | 600 | … | 700 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| INIT | 0 | 0 | … | 0 | … | M | … | M | … | M | … | M | … | M |
| [100, 1] | 0 | 0 | … | 0 | … | 100 | … | 200 | … | 300 | … | M | … | M |
| [200, 30] | ||||||||||||||
| [300, 20] | ||||||||||||||
| [400, 40] |
第二个节点:[200, 30],更新范围:[201, 600]
- 201-500,需要跟上面一行的值比较。举个例子,400这个点,想要到400,需要399加一公里,一公里花的钱,跟我之前的花的相比,哪个更少。当然还是第一个节点便宜,所以不用动。
- 501-600,第一个节点到不了,只能用第二个节点更新啦
| 0 | 1 | … | 200 | … | 300 | … | 400 | … | 500 | 501 | … | 600 | … | 700 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| INIT | 0 | 0 | … | 0 | … | M | … | M | … | M | M | … | M | … | M |
| [100, 1] | 0 | 0 | … | 0 | … | 100 | … | 200 | … | 300 | M | … | M | … | M |
| [200, 30] | 0 | 0 | 0 | 100 | 200 | … | 300 | 330 | … | 3300 | … | M | |||
| [300, 20] | |||||||||||||||
| [400, 40] |
第三个节点:[300, 20],更新范围:[301, 700]
- 301-500,仍然比不过节点1
- 501-600,以501举例,第三个节点的花销应该是,500的花销+20,也就是320,而前面我们推导出来,是330,所以将这个值更新为320.
| 0 | 1 | … | 200 | … | 300 | … | 400 | … | 500 | 501 | … | 600 | … | 700 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| INIT | 0 | 0 | … | 0 | … | M | … | M | … | M | M | … | M | … | M |
| [100, 1] | 0 | 0 | … | 0 | … | 100 | … | 200 | … | 300 | M | … | M | … | M |
| [200, 30] | 0 | 0 | 0 | 100 | 200 | … | 300 | 330 | … | 3300 | … | M | |||
| [300, 20] | 0 | 0 | 0 | 100 | 200 | 300 | 320 | 2300 | 4300 | ||||||
| [400, 40] |
第四个节点:[400, 40],更新范围:[401, 700],最远就到700,哪来的800呢。
没有截止到第三个节点的划算,所以不更新。
| 0 | 1 | … | 200 | … | 300 | … | 400 | … | 500 | 501 | … | 600 | … | 700 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| INIT | 0 | 0 | … | 0 | … | M | … | M | … | M | M | … | M | … | M |
| [100, 1] | 0 | 0 | … | 0 | … | 100 | … | 200 | … | 300 | M | … | M | … | M |
| [200, 30] | 0 | 0 | 0 | 100 | 200 | … | 300 | 330 | … | 3300 | … | M | |||
| [300, 20] | 0 | 0 | 0 | 100 | 200 | 300 | 320 | 2300 | 4300 | ||||||
| [400, 40] | 0 | 0 | 0 | 100 | 200 | 300 | 320 | 2300 | 4300 |
那我们可以把递推公式写出来了:
:前一公里的价钱+该节点的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);
}
}
总结
动态规划真好用,之前做绿洲之旅那道题,还没有学到动态规划,还在用单调队列解决问题,然后刷到这道题,一脸懵逼,怎么如此复杂,就先搁置了。现在回过头看看,用背包的思想真的很简单。刷题!刷题!还差图论和回溯,就基本上学完啦,加油!