题目
给你一个整数 hoursBefore
,表示你要前往会议所剩下的可用小时数。要想成功抵达会议现场,你必须途经 n
条道路。道路的长度用一个长度为 n
的整数数组 dist
表示,其中 dist[i]
表示第 i
条道路的长度(单位:千米)。另给你一个整数 speed
,表示你在道路上前进的速度(单位:千米每小时)。
当你通过第i
条路之后,就必须休息并等待,直到 下一个整数小时 才能开始继续通过下一条道路。注意:你不需要在通过最后一条道路后休息,因为那时你已经抵达会议现场。
- 例如,如果你通过一条道路用去
1.4
小时,那你必须停下来等待,到2
小时才可以继续通过下一条道路。如果通过一条道路恰好用去2
小时,就无需等待,可以直接继续。
然而,为了能准时到达,你可以选择 跳过 一些路的休息时间,这意味着你不必等待下一个整数小时。注意,这意味着与不跳过任何休息时间相比,你可能在不同时刻到达接下来的道路。
- 例如,假设通过第
1
条道路用去1.4
小时,且通过第2
条道路用去0.6
小时。跳过第1
条道路的休息时间意味着你将会在恰好2
小时完成通过第2
条道路,且你能够立即开始通过第3
条道路。
返回准时抵达会议现场所需要的 最小跳过次数 ,如果 无法准时参会 ,返回-1
。
示例
示例1
输入:dist = [1,3,2], speed = 4, hoursBefore = 2
输出:1
解释:
不跳过任何休息时间,你将用 (1/4 + 3/4) + (3/4 + 1/4) + (2/4) = 2.5 小时才能抵达会议现场。
可以跳过第 1 次休息时间,共用 ((1/4 + 0) + (3/4 + 0)) + (2/4) = 1.5 小时抵达会议现场。
注意,第 2 次休息时间缩短为 0 ,由于跳过第 1 次休息时间,你是在整数小时处完成通过第 2 条道路。
示例2
输入:dist = [7,3,5,5], speed = 2, hoursBefore = 10
输出:2
解释:
不跳过任何休息时间,你将用 (7/2 + 1/2) + (3/2 + 1/2) + (5/2 + 1/2) + (5/2) = 11.5 小时才能抵达会议现场。
可以跳过第 1 次和第 3 次休息时间,共用 ((7/2 + 0) + (3/2 + 0)) + ((5/2 + 0) + (5/2)) = 10 小时抵达会议现场。
示例3
输入:dist = [7,3,5,5], speed = 1, hoursBefore = 10
输出:-1
解释:即使跳过所有的休息时间,也无法准时参加会议。
提示
- n = = d i s t . l e n g t h n == dist.length n==dist.length
- 1 < = n < = 1000 1 <= n <= 1000 1<=n<=1000
- 1 < = d i s t [ i ] < = 1 0 5 1 <= dist[i] <= 10^5 1<=dist[i]<=105
- 1 < = s p e e d < = 1 0 6 1 <= speed <= 10^6 1<=speed<=106
- 1 < = h o u r s B e f o r e < = 1 0 7 1 <= hoursBefore <= 10^7 1<=hoursBefore<=107
题解
方法一:动态规划
思路与算法
我们用 f [ i ] [ j ] f[i][j] f[i][j] 表示经过了 d i s t [ 0 ] dist[0] dist[0] 到 d i s t [ i − 1 ] dist[i−1] dist[i−1] 的 i i i 段道路,并且跳过了 j j j次的最短用时。
在进行状态转移时,我们考虑最后一段道路 d i s t [ i − 1 ] dist[i−1] dist[i−1] 是否选择跳过:
- 如果没有跳过,那么状态转移方程为:
f [ i ] [ j ] = ⌈ f [ i − 1 ] [ j ] + d i s t [ i − 1 ] s p e e d ⌉ f[i][j]=\lceil f[i-1][j]+\frac{dist[i-1]}{speed} \rceil f[i][j]=⌈f[i−1][j]+speeddist[i−1]⌉
其中 ⌈ x ⌉ \lceil x \rceil ⌈x⌉ 表示将 x x x 向上取整。对于最后一段道路,我们无需等待到下一个整数小时,但由于题目中给定的 hoursBefore \textit{hoursBefore} hoursBefore是一个整数,因此在状态转移方程中向上取整是不会影响结果的。 - 如果跳过,那么状态转移方程为:
f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + d i s t [ i − 1 ] s p e e d f[i][j]=f[i-1][j-1]+\frac{dist[i-1]}{speed} f[i][j]=f[i−1][j−1]+speeddist[i−1]
由于我们到达的时间尽可能早,因此需要选择这两种转移中的较小值,即:
f
[
i
]
[
j
]
=
m
i
n
{
⌈
f
[
i
−
1
]
[
j
]
+
d
i
s
t
[
i
−
1
]
s
p
e
e
d
⌉
,
f
[
i
−
1
]
[
j
−
1
]
+
d
i
s
t
[
i
−
1
]
s
p
e
e
d
}
f[i][j]=min\{\lceil f[i-1][j]+\frac{dist[i-1]}{speed} \rceil,f[i-1][j-1]+\frac{dist[i-1]}{speed}\}
f[i][j]=min{⌈f[i−1][j]+speeddist[i−1]⌉,f[i−1][j−1]+speeddist[i−1]}
需要注意的是,当 j = 0 j=0 j=0 时,我们不能通过「跳过」进行转移;当 j = i j=i j=i 时,我们不能通过「没有跳过」进行转移;当 j > i j>i j>i 时,我们无法在 i i i 段道路内跳过超过 i i i 次,对应的状态不合法。
当我们计算完所有状态的值后,我们只需要找到最小的 j j j,使得 f [ n ] [ j ] ≤ hoursBefore f[n][j] \leq \textit{hoursBefore} f[n][j]≤hoursBefore,这个 j j j 即为最少需要跳过的次数。如果不存在这样的 j j j,那么返回 − 1 −1 −1。
动态规划的细节
动态规划的边界条件为
f
[
0
]
[
0
]
=
0
f[0][0] = 0
f[0][0]=0,表示初始(未走过任何道路)时的时间为
0
0
0。
由于状态转移方程中的取值为 m i n min min,因此我们可以将除了 f [ 0 ] [ 0 ] f[0][0] f[0][0] 以外所有的状态置为一个极大值 ∞ \infty ∞,方便进行转移。
浮点数运算的细节
根据 IEEE 754 标准,浮点数在计算机中存储的精度是有限的,而本题中我们不可避免的会使用「浮点数运算」以及「向上取整」运算,如果强行忽略产生的计算误差,会得到错误的结果。
举一个简单的例子,假设使用的语言中「向上取整」运算的函数为
ceil
\texttt{ceil}
ceil,下面的运算:
c
e
i
l
(
8.0
+
1.0
/
3
+
1.0
/
3
+
1.0
/
3
)
ceil(8.0 + 1.0 / 3 + 1.0 / 3 + 1.0 / 3)
ceil(8.0+1.0/3+1.0/3+1.0/3)
应当是
9
9
9,而计算机会给出
10
10
10。这是因为浮点数误差导致:
8.0
+
1.0
/
3
+
1.0
/
3
+
1.0
/
3
8.0 + 1.0 / 3 + 1.0 / 3 + 1.0 / 3
8.0+1.0/3+1.0/3+1.0/3
计算出的结果约为:
9.000000000000002
9.000000000000002
9.000000000000002
向上取整后会得到 10 10 10,产生了错误的答案。
因此我们引入常量 eps \text{eps} eps 表示一个极小值,例如 1 0 − 8 10^{-8} 10−8 。在进行「向上取整」运算前,我们将待取整的浮点数减去 eps \text{eps} eps再进行取整,就可以避免上述的误差。
同时,我们需要说明 eps \text{eps} eps不会引入其它的问题。在本题中速度最大为 1 0 6 10^6 106,时间与速度成反比,那么 e p s eps eps的粒度只需要高于时间的精度 1 0 − 6 10^{-6} 10−6即可,取 1 0 − 7 10^{-7} 10−7或 1 0 − 8 10^{-8} 10−8都是可行的。
最后在比较 f [ n ] [ j ] ≤ hoursBefore f[n][j] \leq \textit{hoursBefore} f[n][j]≤hoursBefore 时,我们也需要将左侧减去 eps \text{eps} eps 或将右侧加上 eps \text{eps} eps,再进行比较。
代码
class Solution:
def minSkips(self, dist: List[int], speed: int, hoursBefore: int) -> int:
# 可忽略误差
EPS = 1e-7
n = len(dist)
f = [[float("inf")] * (n + 1) for _ in range(n + 1)]
f[0][0] = 0.
for i in range(1, n + 1):
for j in range(i + 1):
if j != i:
f[i][j] = min(f[i][j], ceil(f[i - 1][j] + dist[i - 1] / speed - EPS))
if j != 0:
f[i][j] = min(f[i][j], f[i - 1][j - 1] + dist[i - 1] / speed)
for j in range(n + 1):
if f[n][j] < hoursBefore + EPS:
return j
return -1
复杂度分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( n 2 ) O(n^2) O(n2),即为存储所有状态需要的空间。
方法二:动态规划 + 将所有运算变为整数运算
思路与算法
我们可以将数组 dist \textit{dist} dist中的道路长度和 hoursBefore \textit{hoursBefore} hoursBefore都乘以 speed \textit{speed} speed。由于方法一的代码中所有除法运算的除数都是 speed \textit{speed} speed,因此这样做可以保证所有的除法运算的结果都是整数,从根本上避免浮点数误差。
但需要注意的是,在题目中我们规定「必须休息并等待,直到下一个整数小时才能开始继续通过下一条道路」,那么当我们将上面的变量都乘以 speed \textit{speed} speed后,规定应当变更为「必须休息并等待,直到下一个 speed \textit{speed} speed的倍数小时才能开始继续通过下一条道路」。其余的逻辑与方法一完全相同。
细节
时间 x x x 的下一个 speed \textit{speed} speed的倍数小时为: ( ⌊ x − 1 s p e e d ⌋ + 1 ) ⋅ s p e e d (\lfloor \frac{x-1}{speed} \rfloor +1) \cdot speed (⌊speedx−1⌋+1)⋅speed
其中 ⌊ x ⌋ \lfloor x \rfloor ⌊x⌋ 表示将 x x x向下取整。
代码
class Solution:
def minSkips(self, dist: List[int], speed: int, hoursBefore: int) -> int:
n = len(dist)
f = [[float("inf")] * (n + 1) for _ in range(n + 1)]
f[0][0] = 0
for i in range(1, n + 1):
for j in range(i + 1):
if j != i:
f[i][j] = min(f[i][j], ((f[i - 1][j] + dist[i - 1] - 1) // speed + 1) * speed)
if j != 0:
f[i][j] = min(f[i][j], f[i - 1][j - 1] + dist[i - 1])
for j in range(n + 1):
if f[n][j] <= hoursBefore * speed:
return j
return -1
复杂度分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( n 2 ) O(n^2) O(n2),即为存储所有状态需要的空间。