#LeetCode刷题笔记# 983 最低票价 (python版)

题目来源:力扣(LeetCode)

在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。
在接下来的一年里,你要旅行的日子将以一个名为 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

拿到这题,没思路。 看完官方解答,(代码)不懂…,不熟悉python的一些用法。emmmmm
怪自己太菜。手码一下,以理清思路。


解法1的思路:

由题目可知, d a y s days days的取值范围为[1,365],长度也在[1,365]内,也就是说我们只用考虑365天内的。
d p [ i ] dp[i] dp[i]表示第 i i i天到365天我们最少需要花费的钱。因此 d p [ 1 ] dp[1] dp[1]为我们要求的结果。
在这一年中,并不是每一天都要买票,按照贪心的思想,越晚买票越好,因此只有在旅游日,我们才有可能会去买票。
假设我们已经求得了 d p [ i ] dp[i] dp[i],若第 i − 1 i-1 i1天不是旅游日,则不需要买票,因此有:
d p [ i − 1 ] = d p [ i ] dp[i-1] = dp[i] dp[i1]=dp[i]
若第 i − 1 i-1 i1天需要买票,我们可以选择第 i − 1 i-1 i1天买1天的通行证,那么此时的花费为 c o s t [ 0 ] + d p [ i ] cost[0]+dp[i] cost[0]+dp[i],但这不是唯一的方式,我们同样可以选择买7天的通行证。但是若第 i − 1 i-1 i1天买了7天的通行证,那我们在第 i i i至第 i + 5 i+5 i+5天内都不需要买票了,因此此时相对的最小花费为 c o s t [ 1 ] + d p [ i + 6 ] cost[1]+dp[i+6] cost[1]+dp[i+6]。同理若在第 i − 1 i-1 i1天买了30天的通行证,则此时相对的最小花费为 c o s t [ 2 ] + d p [ i + 29 ] cost[2]+dp[i+29] cost[2]+dp[i+29],我们应该在这三种方案中选择一个花费最小的,因此有:
d p [ i − 1 ] = m i n ( c o s t [ 0 ] + d p [ i ] , c o s t [ 1 ] + d p [ i + 6 ] , c o s t [ 2 ] + d p [ i + 29 ] ) dp[i-1] = min(cost[0]+dp[i],cost[1]+dp[i+6],cost[2]+dp[i+29]) dp[i1]=min(cost[0]+dp[i],cost[1]+dp[i+6],cost[2]+dp[i+29])
设一个数组 l a s t last last l a s t [ j ] last[j] last[j]表示第 j j j张通行证可游玩 l a s t [ j ] last[j] last[j]天。即, l a s t [ 0 ] = 1 last[0] = 1 last[0]=1 l a s t [ 1 ] = 7 last[1] = 7 last[1]=7 l a s t [ 2 ] = 30 last[2] = 30 last[2]=30

整理下可得到最终的递推表达式:
d p [ i ] = { d p [ i + 1 ] , 若 第 i 天 不 是 旅 游 日 m i n ( c o s t [ j ] + d p [ i + l a s t [ j ] ] ) , 若 第 i 天 是 旅 游 日 , j = 1 , 2 , 3 dp[i] = \left\{ \begin{array}{ll} dp[i+1],&若第i天不是旅游日\\ \mathrm{min}(cost[j]+dp[i+last[j]]),&若第i天是旅游日,j = 1,2,3 \end{array} \right. dp[i]={dp[i+1]min(cost[j]+dp[i+last[j]])iij=1,2,3

以示例1为例,days = [1,4,6,7,8,20], costs = [2,7,15]
i > 20 i > 20 i>20时, d p [ i ] = 0 dp[i] = 0 dp[i]=0
所以: d p [ 20 ] = m i n ( c o s t [ 0 ] + d p [ 21 ] , c o s t [ 1 ] + d p [ 27 ] , c o s t [ 2 ] + d p [ 50 ] ) = 2 dp[20] = min(cost[0]+dp[21],cost[1]+dp[27],cost[2]+dp[50]) = 2 dp[20]=min(cost[0]+dp[21],cost[1]+dp[27],cost[2]+dp[50])=2
第9-19天都不是旅游日,因此:
8 < i < 20 8<i<20 8<i<20时,dp[i] = dp[20] = 2
i = 8 i = 8 i=8时,需要买票了
d p [ 8 ] = m i n ( c o s t [ 0 ] + d p [ 9 ] , c o s t [ 1 ] + d p [ 15 ] , c o s t [ 2 ] + d p [ 38 ] ) = 4 dp[8] = min(cost[0]+dp[9],cost[1]+dp[15],cost[2]+dp[38]) = 4 dp[8]=min(cost[0]+dp[9],cost[1]+dp[15],cost[2]+dp[38])=4
依次类推有:
d p [ 7 ] = m i n ( c o s t [ 0 ] + d p [ 8 ] , c o s t [ 1 ] + d p [ 14 ] , c o s t [ 2 ] + d p [ 37 ] ) = 6 dp[7] = min(cost[0]+dp[8],cost[1]+dp[14],cost[2]+dp[37]) = 6 dp[7]=min(cost[0]+dp[8],cost[1]+dp[14],cost[2]+dp[37])=6
d p [ 6 ] = m i n ( c o s t [ 0 ] + d p [ 7 ] , c o s t [ 1 ] + d p [ 13 ] , c o s t [ 2 ] + d p [ 36 ] ) = 8 dp[6] = min(cost[0]+dp[7],cost[1]+dp[13],cost[2]+dp[36]) = 8 dp[6]=min(cost[0]+dp[7],cost[1]+dp[13],cost[2]+dp[36])=8
d p [ 5 ] = d p [ 6 ] = 8 dp[5] = dp[6] = 8 dp[5]=dp[6]=8
d p [ 4 ] = m i n ( c o s t [ 0 ] + d p [ 5 ] , c o s t [ 1 ] + d p [ 11 ] , c o s t [ 2 ] + d p [ 34 ] ) = 9 dp[4] = min(cost[0]+dp[5],cost[1]+dp[11],cost[2]+dp[34]) = 9 dp[4]=min(cost[0]+dp[5],cost[1]+dp[11],cost[2]+dp[34])=9
d p [ 2 ] = d p [ 3 ] = d p [ 4 ] = 9 dp[2] = dp[3] = dp[4] = 9 dp[2]=dp[3]=dp[4]=9
d p [ 1 ] = m i n ( c o s t [ 0 ] + d p [ 2 ] , c o s t [ 1 ] + d p [ 8 ] , c o s t [ 2 ] + d p [ 31 ] ) = 11 dp[1] = min(cost[0]+dp[2],cost[1]+dp[8],cost[2]+dp[31]) = 11 dp[1]=min(cost[0]+dp[2],cost[1]+dp[8],cost[2]+dp[31])=11

接下来看下代码实现
以下是按官方解答默写的Python3代码

# 解法1
class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        last = [1,7,30]

        @lru_cache(None)
        def dp(i):
            if i > 365:
                return 0
            elif i in days:
                return min(c+dp(i+l) for c, l in zip(costs, last))
            else:
                return dp(i+1)
        return dp(1)
代码说明
  • 这里采用了递归。
  • @lru_cache(None) 是缓存装饰器。

lru_cache(maxsize=128, typed=False)

  • maxsize表示最大缓存数。若maxsize=None,则禁用LRU功能,并且缓存可以无限制增长;当maxsize= 2 m 2^m 2m时,LRU功能执行得最好;
  • 若typed=True, 则不同类型的函数参数将单独缓存。例如,f(3)和f(3.0)将被视为具有不同结果的不同调用;
  • 缓存是有内存存储空间限制的;

使用这个装饰器后,可以保存函数的调用结果,@lru_cache(None) 装饰器适合于耗时的函数。
ps: LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法

  • zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。

可以很容易地将其改为非递归形式:

# 版本1, 使用了长度为396的数组dp,第一个元素没有意义,
# 后面多余的30个元素只是为了循环时不超出索引限制
# 该版本简单好实现,但是运行较慢,浪费的空间也比较多
class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        last = [1, 7, 30]
        dp = [0 for _ in range(396)]
        for i in reversed(range(366)):
            if i not in days:
                dp[i] = dp[i+1]
            else:
                dp[i] = min(c+dp[i+l] for c, l in zip(costs, last))
        return dp[1]
# 版本2,其实从days[-1]天开始计算即可
class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        last = [1, 7, 30]
        dp = [0 for _ in range(366)]
        for i in reversed(range(1,days[-1]+1)):
            if i not in days:
                dp[i] = dp[i+1]
            else:
                dp[i] = min(c+dp[(i+l)%365] for c, l in zip(costs, last))
        return dp[1]

也可以将dp倒过来存储结果,此时dp[days[-1]]为最终答案。

#版本3
class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        dp = [0 for _ in range(366)]
        for i in range(1,days[-1]+1):   # days[-1]天之后的就不用再计算了
            if i not in days:
                dp[i] = dp[i-1]
            else:
                dp[i] = min(costs[0]+dp[i-1],costs[1]+dp[i-7],costs[2]+dp[i-30])
                
        return dp[days[-1]]

解法2的思路:

在解法1中,需要计算不旅行日子到年末的最小花费, d p [ i ] = d [ i + 1 ] = d p [ i + 2 ] = … dp[i] = d[i+1] = dp[i+2] = … dp[i]=d[i+1]=dp[i+2]=这些计算其实都是不需要的。可对其进行改进。
d p [ i ] dp[i] dp[i] 表示第 d a y s [ i ] days[i] days[i]天到最后一个旅行日 d a y s [ − 1 ] days[-1] days[1]所需的最小花费。
假设 j 1 j_1 j1表示满足 d a y s [ j 1 ] > = d a y s [ i ] + 1 days[j_1] >= days[i] +1 days[j1]>=days[i]+1的最小下标, j 2 j_2 j2表示满足 d a y s [ j 2 ] > = d a y s [ i ] + 7 days[j_2] >= days[i] +7 days[j2]>=days[i]+7 j 3 j_3 j3表示满足 d a y s [ j 3 ] > = d a y s [ i ] + 30 days[j_3] >= days[i] +30 days[j3]>=days[i]+30
例如对于, d a y s = [ 1 , 4 , 6 , 7 , 8 , 20 , 40 ] , c o s t s = [ 2 , 7 , 15 ] days = [1,4,6,7,8,20,40], costs = [2,7,15] days=[1,4,6,7,8,20,40],costs=[2,7,15]
假设 i = 2 i = 2 i=2 d a y s [ i ] = d a y s [ 2 ] = 6 days[i] = days[2] = 6 days[i]=days[2]=6,那么 j 1 = 3 , j 2 = 5 , j 3 = 6 j_1 = 3, j_2 = 5, j_3 = 6 j1=3,j2=5,j3=6
那么由解法1很容易得到:
d p [ i ] = m i n ( c o s t [ 0 ] + d p [ j 1 ] , c o s t [ 1 ] + d p [ j 2 ] , c o s t [ 2 ] + d p [ j 3 ] ) dp[i] = min(cost[0]+dp[j_1], cost[1]+dp[j_2], cost[2]+dp[j_3]) dp[i]=min(cost[0]+dp[j1],cost[1]+dp[j2],cost[2]+dp[j3])
该解法中返回的值应为dp[0]。
代码如下:

class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        N = len(days)
        last = [1, 7 ,30]

        @lru_cache(None)
        def dp(i):
            if i == N:
                return 0
            
            res = float('inf')     # 初始化结果为无穷大
            for c, l in zip(costs, last):
                j = i
                while j < N and days[j] < days[i] + l:  # 找j1, j2, j3
                    j += 1
                res = min(res, c+dp(j))    # 递归
            return res
        return dp(0)

也可将以上代码改为非递归形式:

class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        N = len(days)
        last = [1, 7, 30]
        dp = [0 for _ in range(N)]
        
        for i in reversed(range(N)):
            res = float('inf')
            if i == N-1:
                dp[i] = min(costs)  # 有个测例给的costs居然不是严格递增的- -
            else:
                j = i
                for c, l in zip(costs, last):
                    while j < N and days[j] < days[i] + l:
                        j = j + 1
                    if j < N:
                        res = min(res,c+dp[j])
                    else:
                        res = min(res,c)  # j可能越界
                    j = i
                dp[i] = res
        return dp[0]

还有其他解法,以后再说吧(捂脸T^T)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值