题目:
在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 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,并完成了你计划的每一天旅行。
示例 2:
输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]
输出:17
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[2] = $15 买了一张为期 30 天的通行证,它将在第 1, 2, ..., 30 天生效。
在第 31 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 31 天生效。
你总共花了 $17,并完成了你计划的每一天旅行。
提示:
1 <= days.length <= 365
1 <= days[i] <= 365
days
按顺序严格递增costs.length == 3
1 <= costs[i] <= 1000
自顶向下的递归执行动态规划
思路:
一、寻找子问题
假设第 100 天是旅行的最后一天,分类讨论:
在第 100 天购买为期 1 天的通行证,接下来需要解决的问题为:1 到 99 天的最小花费。
在第 94 天购买为期 7 天的通行证,接下来需要解决的问题为:1 到 93 天的最小花费。
在第 71 天购买为期 30 天的通行证,接下来需要解决的问题为:1 到 70 天的最小花费。
这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。
注 1:从右往左思考,主要是为了方便把递归翻译成递推。从左往右思考也是可以的。
注 2:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。在做题时,可根据题目要求,选择适合题目的一种来思考。本题用到的是「枚举选哪个」。
二、状态定义与状态转移方程
根据上面的讨论,定义 dfs(i) 表示 1 到 i 天的最小花费。
如果第 i 天不在 days 中,那么问题变成 1 到 i − 1 天的最小花费,即 dfs(i) = dfs(i − 1)
如果第 i 天在 days 中,分类讨论:
在第 i 天购买为期 1 天的通行证,接下来需要解决的问题为:1 到 i−1 天的最小花费,即 dfs(i) = dfs(i −1) + costs[0]。
在第 i − 6 天购买为期 7 天的通行证,接下来需要解决的问题为:1 到 i − 7 天的最小花费,即 dfs(i) = dfs(i − 7) + costs[1]。
在第 i − 29 天购买为期 30 天的通行证,接下来需要解决的问题为:1 到 i − 30 天的最小花费,即 dfs(i) = dfs(i − 30) + costs[2]。
这三种情况取最小值,就得到了 dfs(i),即dfs(i) = min(dfs(i − 1) + costs[0], dfs(i − 7) + costs[1], dfs(i − 30) + costs[2])
递归边界:dfs(i) = 0,其中 i ≤ 0。此时没有要旅行的天数。
递归入口:dfs(D),其中 D = days[n − 1] 是最后一天。为了方便翻译成递推,我们从最后一天开始思考。
三、递归搜索 + 保存递归返回值 = 记忆化搜索
考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 memo 数组中。
如果一个状态不是第一次遇到(memo 中保存的结果不等于 memo 的初始值),那么可以直接返回 memo 中保存的结果。
注意:memo 数组的初始值一定不能等于要记忆化的值!例如初始值设置为 0,并且要记忆化的 dfs(i) 也等于 0,那就没法判断 0 到底表示第一次遇到这个状态,还是表示之前遇到过了,从而导致记忆化失效。一般把初始值设置为 −1。
本题由于 costs[i] 均为正数,不会算出 0,把初始值设置为 0 也可以。
代码:
class Solution {
public:
int mincostTickets(vector<int>& days, vector<int>& costs) {
int last_day = days.back();
unordered_set<int> day_set(days.begin(), days.end());
vector<int> memo(last_day + 1);
auto dfs = [&](auto&& dfs, int i) -> int {
if (i <= 0) {
return 0;
}
int& res = memo[i]; // 注意这里是引用
if (res) { // 之前计算过
return res;
}
if (!day_set.count(i)) {
return res = dfs(dfs, i - 1);
}
return res = min({dfs(dfs, i - 1) + costs[0],
dfs(dfs, i - 7) + costs[1],
dfs(dfs, i - 30) + costs[2]});
};
return dfs(dfs, last_day);
}
};