一、问题描述
在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为days的数组给出。每一项是一个从1到365的整数。
火车票有三种不同的销售方式:
- 一张为期一天的通行证售价为costs[0]美元;
- 一张为期七天的通行证售价为costs[1]美元;
- 一张为期三十天的通行证售价为costs[2]美元。
通行证允许数天无限制的旅行。例如,如果我们在第2天获得一张为期7天的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。
返回 你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费 。
示例1:
输入:days = [1,4,6,7,8,20], costs = [2,7,15]
输出:11
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。
在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, ..., 9 天生效。
在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。
你总共花了 $11,并完成了你计划的每一天旅行。
提示:
二、解题方法
看到题目首先思考的是贪心和动态规划两种方法,但是看到数组长度范围咋1-365,果断选择了动态规划。
2.1 动态规划
下面给出简要的解题思路:
- 用一个dp数组来记录已完成旅行天数的最小花费,dp[i]代表日期days[0]~days[i-1]完成的最小花费钱数;
- base case:days[0]之前都不存在旅游行为,花费为0,所以dp[0]=0;
- 对于dp[i],对应包含days[i-1]这一天在内之前的时间段days[0]~days[i-1]均以完成旅行,那么之前的某一天j一定进行了决策:
- 买入了1日票:花费为dp[j] + costs[0], 条件为days[i-1] - days[j] < 1;
- 买入了7日票:花费为dp[j] + costs[1], 条件为days[i-1] - days[j] < 7;
- 买入了30日票:花费为dp[j] + costs[2], 条件为days[i-1] - days[j] < 30; - dp[i]是上述情况中的最小值,最后返回结果dp[days.length]即可;
代码如下:
/**
* @param {number[]} days
* @param {number[]} costs
* @return {number}
*/
var mincostTickets = function(days, costs) {
let dp=days.slice().fill(0);
dp.push(0); //这里就是把dp[0]写入进去
let [p1,p7,p30]=costs;
for(let i=1;i<dp.length;i++){ //这里下标要从1开始,要是从0开始就会用infinity覆盖之前的dp[0]=0,永远找不到最小值
let min=Infinity;
for(let j=i-1;j>=0&&days[i-1]-days[j]<30;j--]){
if(days[i-1]-days[j]<1)min=Math.min(dp[j]+p1,min);
if(days[i-1]-days[j]<7)min=Math.min(dp[j]+p7,min);
min=Math.min(dp[j]+p30,min);
}
dp[i]=min;
}
return dp[dp.length-1];
}
看题解的时候还发现另一个从后向前的倒序计算方法,其解题思路如下:
- dp[i]设置为从第i天开始到最后一天的最小票价
- 从最后一天开始往第一天循环,中间遇到不需要旅游,则当天花费=前一天花费,即dp[i]=dp[i+1]
- 设置一个days的下标变量k,当i=days[k]时,则说明第i天需要旅游,则这时需要计算最小花费:dp[i]=Math.min(dp[i+1]+cost[0],dp[i+7]+cost[1],dp[i+30]+cost[2])。同时将k下标前移一位k–
- 最后,i循环到1,算出最小票价,取days数组第一位dp[0]的花费即可。
/**
* @param {number[]} days
* @param {number[]} costs
* @return {number}
*/
var mincostTickets = function(days, costs) {
let dp=new Array(366+30).fill(0);
n=days.length;
maxDay=days[n-1];
minDay=days[0];
k=n-1;
for(let i=maxDay;i>=minDay;i--){
if(i===days[k]){
dp[i]=Math.min(dp[i+1]+costs[0],dp[i+7]+costs[1],dp[i+30]+costs[2]);
k--;}else{
dp[i]=dp[i+1]
}
}
return dp[minDay];
}
三、知识补充
javaScript中的无穷数(Infinity)
Infinity在JS中是一个特殊的数字,其特性为:比任何有限的数字都要大。可分为两种:正无穷和负无穷,JS中对应表达方式为:+Infinity(或者Infinity) 和 -Infinity。
当我们需要初始化涉及数字比较的计算时,无穷值就非常方便。例如,在数组中搜索最小值时:
function findMin(array) {
let min = Infinity;
for (const item of array) {
min = Math.min(min, item);
}
return min;
}
findMin([5, 2, 1, 4]); // => 1
min变量使用Infinity初始化。在第一次for()迭代中,最小值成为第一项。