在问 · 算法题解

983. 最低票价

中等动态规划按日历天数 DP力扣原题 ↗

题目描述

输入严格递增的数组 days,列出未来一年中你要出行的日子,每天用 1 到 365 的整数表示。另给长度为 3 的数组 costs。

通行证有三种:1 天票价格 costs[0],7 天票价格 costs[1],30 天票价格 costs[2]。从购买当天起算,在对应天数内可以不限次出行。例如第 2 天买的 7 天票覆盖第 2 天到第 8 天。

返回让 days 中每一天都被某张通行证覆盖的最低总花费。

示例 1
输入:days = [1,4,6,7,8,20], costs = [2,7,15]
输出:11

解释:第 1 天买 1 天票,第 3 天买 7 天票(覆盖 3 到 9),第 20 天买 1 天票,共 11。

示例 2
输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]
输出:17

解释:第 1 天买 30 天票覆盖到第 30 天,第 31 天再买 1 天票,共 17。

数据范围

  • 1 <= days.length <= 365
  • 1 <= days[i] <= 365
  • days 严格递增。
  • costs.length == 3
  • 1 <= costs[i] <= 1000

解题思路:按日历天数 DP

设 dp[d] 为覆盖前 d 天里所有出行日的最小花费。第 d 天不出行,就不用为它买票,dp[d] = dp[d-1]。第 d 天出行,它必定被最后买的那张票覆盖:若是 1 天票,前面要覆盖到 d-1;若是 7 天票(最晚第 d-6 天买也能管到 d),前面只需覆盖到 d-7;30 天票同理。所以 dp[d] = min(dp[d-1]+c0, dp[d-7]+c1, dp[d-30]+c2),下标小于 0 时按 0 算。

为什么正确

dp[d] 只依赖更小的下标,从小到大算时所需的值都已经是最优。

复杂度分析

时间 O(W),空间 O(W),W = days 的最后一天(≤ 365)。

易错点

贪心地“按单价最便宜的票买”不对,要看具体出行日的分布。另外 costs 不一定递增,可能 1 天票反而比 7 天票贵,三种都要比。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        last = days[-1]
        travel = set(days)
        dp = [0] * (last + 1)
        for d in range(1, last + 1):
            if d not in travel:
                dp[d] = dp[d - 1]
                continue
            a = dp[d - 1] + costs[0]
            b = dp[max(0, d - 7)] + costs[1]
            c = dp[max(0, d - 30)] + costs[2]
            dp[d] = min(a, b, c)
        return dp[last]

载入编辑器,自己跑一遍 →

其他解法

按天 DP 对日历上每一天都建状态,转移直接看 d-1、d-7、d-30;按旅行日 DP 只对真正出行的日子建状态,用指针找到票过期后的第一个旅行日,适合日期稀疏的情况。

解法 2:按旅行日下标 DP + 指针 时间 O(n),空间 O(n)

dp[i] 表示从第 i 个旅行日开始,覆盖剩下所有旅行日的最小花费,从后往前计算。在第 i 天买时长为 1、7、30 的票,会覆盖所有 days[j] < days[i] + 时长 的旅行日,所以下一个要买票的位置就是第一个不被覆盖的下标 j。三种票的时长递增,j 只需向后移动,转移为 dp[i] = min(dp[j] + cost)。

状态数只和旅行日个数 n(不超过 365)有关,与日期范围无关,日期跨度很大时依然高效。需要多维护一个指针,不如按天 DP 直观。

class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        n = len(days)
        durations = (1, 7, 30)
        dp = [0] * (n + 1)
        for i in range(n - 1, -1, -1):
            best = float('inf')
            j = i
            for cost, dur in zip(costs, durations):
                while j < n and days[j] < days[i] + dur:
                    j += 1
                best = min(best, dp[j] + cost)
            dp[i] = best
        return dp[0]

动态规划 专题的其他题目

查看 动态规划 全部题目 →