力扣加加题解:1883. 准时抵达会议现场的最小跳过休息次数——动态规划与浮点精度攻防实战
【免费下载链接】leetcodeLeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解,记录自己的leetcode解题之路。)项目地址: https://gitcode.com/gh_mirrors/le/leetcode
导读
本题(LeetCode 1883 / 力扣 5775)考察的是在"逐路行驶 + 整数时刻等待"这一特殊时间模型下,如何通过最少次数的"跳过休息"操作准时抵达会议现场。本文以 problems/5775.minimum-skips-to-arrive-at-meeting-on-time.md 为骨架,完整还原题目模型、动态规划推导、边界处理与浮点精度陷阱,并结合本仓库的 动态规划专题 与 二分查找专题 深入剖析"为什么这道题不能二分、必须 DP",读完你不仅能拿下本题,还能掌握一套"带取整操作 + 浮点精度"类 DP 的通用处理范式。
题目描述与时间模型建模
给定:
dist[i]:第i条道路的长度(单位:千米),共n条道路;speed:行驶速度(单位:千米/小时);hoursBefore:距离开会剩余的可用小时数。
行驶规则如下:
- 通过第
i条路花费时间为dist[i] / speed小时; - 通过第
i条路之后必须休息并等待,直到下一个整数小时才能继续通过下一条路(最后一条路通过后不需要休息,因为已经抵达会场); - 你可以跳过某些道路之后的休息,即不等待到下一个整数小时,立即进入下一条路。
示例说明(来自原题描述):
- 若通过一条路用去
1.4小时,则必须等待到2小时才能继续;若恰好用去2小时,则无需等待。 - 假设通过第 1 条路用去
1.4小时、第 2 条路用去0.6小时:若跳过第 1 条路后的休息,你会在恰好2小时完成第 2 条路,并可立即开始第 3 条路。
要求:返回准时抵达会议现场所需的最小跳过次数;若无论如何都无法准时参会,返回-1。
输入输出示例
| 输入 | 输出 | 解释 |
|---|---|---|
dist = [1,3,2],speed = 4,hoursBefore = 2 | 1 | 不跳过休息需2.5小时;跳过第 1 次休息后需1.5小时 |
dist = [7,3,5,5],speed = 2,hoursBefore = 10 | 2 | 跳过第 1、3 次休息,恰好10小时抵达 |
dist = [7,3,5,5],speed = 1,hoursBefore = 10 | -1 | 即使跳过所有休息,也无法准时参会 |
数据范围
n == dist.length 1 <= n <= 1000 1 <= dist[i] <= 10^5 1 <= speed <= 10^6 1 <= hoursBefore <= 10^7数据规模决定了算法上界:
n最大 1000,暗示O(n^2)级别的动态规划是可行方向;而dist[i]与speed的跨度决定了过程中必然出现非整数耗时,为后面的浮点精度问题埋下伏笔。
思路推演:为什么"能力检测二分"行不通
拿到"最小跳过次数"这类最值问题,第一直觉往往是能力检测二分(又称"可行性二分")——即二分答案k,检查"跳过k次休息是否可行"。
原文档作者明确否定了这一思路,理由非常关键:
possible(rest_count)实现起来复杂度太高。因为rest_count的分布情况是不确定的。令 dist 长度为 n,rest_count为 r,那么分布情况就有 $C_{n}^{r}$ 种。这种枚举显然是不合适的。
也就是说:可行性判定本身需要穷举"哪 r 条路后面跳过休息"的所有组合,而组合数 $C_n^r$ 在n = 1000时是天文数字。二分的 $O(\log n)$ 层数救不了内部指数级的判定成本,因此二分路线整体不可行。
对"能力检测二分"范式感兴趣的读者,可对照本仓库的 二分查找专题(内含模板与典型应用),体会其适用前提:可行性判定必须是多项式时间,否则二分只会放大开销。
排除二分之后,自然转向动态规划——把"分布情况"这一不确定因素收敛进状态维度,正是 DP 的强项。
动态规划解法:状态定义与转移方程
状态定义
令dp[i][j]表示到达dist[i-1](即第 i 条路的终点)且已经休息了 j 次(第 j 次休息完毕)所需要的时间。
- 第一维
i:从 1 到n,对应逐条道路推进,天然构成 DP 的阶段; - 第二维
j:0 <= j <= i,记录已使用的"跳过/休息"次数。
这与仓库 动态规划专题 反复强调的核心方法论完全一致:状态定义是动态规划的灵魂。本题之所以把"休息次数"纳入状态,正是因为它既影响后续时刻的整数对齐关系(进而影响总耗时),又正是题目所求的答案维度——"休息次数"既是约束又是目标,一举两得。
转移方程
对第i条路(长度为cur = dist[i-1]),有两种选择:
第 j 次休息(不跳过,等待到整数小时):
dp[i][j] = dp[i-1][j] + ceil(cur / s)即先到达第 i-1 条路终点(此时已休息 j 次),走完第 i 条路后向上取整到下一个整数小时。
第 j 次不休息(跳过等待,直接进入下一条路):
dp[i][j] = dp[i-1][j-1] + cur即到达第 i-1 条路终点时只休息了 j-1 次,本次选择跳过,直接累加真实行驶时间
cur(注意这里cur需要与dp保持同一量纲,见下文精度处理)。
两者取最小值:dp[i][j] = min(休息方案, 不休息方案)。
边界条件
dp[0][0] = 0:起点状态,未出发、未休息,耗时 0;j从 0 枚举到i(休息次数不可能超过已走过的道路数);j == 0时不能选择"不休息":因为每次不休息都意味着在某个路口跳过了休息,而跳过休息本身就是一次计数;若 j 为 0,则前 i 条路之后都不能有跳过行为,第 i 条路之后的等待必须发生(除非是最后一条路),因此dp[i][0]只能由"休息"分支转移而来。这正是原文档强调的"j == 0 不能选择不休息,因为这不符合题意"。
答案的提取
由于题目要求最小的休息次数,从小到大枚举j,一旦出现dp[n][j] <= hoursBefore即说明 j 次休息已足够准时,直接返回该j。若全部枚举完毕仍不满足,返回-1。
浮点精度陷阱:0.3333…的累积灾难
原文档给出了一个非常典型的反例:
0.33333xxx33 + 0.33333xx33 + 0.3333xx33 = 1.0000000000xx002三个 1/3 的真实和是1,但在 IEEE 754 浮点数下计算出的结果会略大于1。本题的"休息 = 向上取整"操作会把这种误差放大为错误:
- 真实时间恰好是整数小时,本可以直接继续(无需等待);
- 但浮点累积后得到
1.0000000000xx002,向上取整变成2,导致多计了近乎 1 个小时; - 原本可以准时到达的方案被误判为超时,答案就可能偏大甚至返回
-1。
两种常见解法
- 化小数为整数(原文档采用):把时间全部乘以
speed,让所有运算在整数域进行,从根本上消灭浮点误差; - 设置精度阈值:若两个数之差小于某个精度值(如
1e-9)则视为相等,配合取整前做微小修正。
整数化的具体实现
关键技巧(原文档点明):
(cur + s - 1) // s ≡ math.ceil(cur / s)即"向上取整"可以用整数运算(cur + s - 1) // s精确实现(//为整除、/为实数除法)。将dp[i][j]整体乘以s之后:
- "休息"分支:
dp[i][j] = (dp[i-1][j] + cur + s - 1) // s * s—— 先做整数域的向上取整对齐,再乘回s保持量纲一致(此时cur也乘以了s); - "不休息"分支:
dp[i][j] = min(dp[i][j], dp[i-1][j-1] + cur)—— 直接累加,不取整; - 可行性判定:
dp[n][j] <= hoursBefore * s,比较双方都在整数域进行,判定结果精确无歧义。
由于题目求最小值,dp数组可全部初始化为一个足够大的值,原文档给出的方案是s * h + 1(任何合法状态都不可能超过该值的上界,数学上等价于"无穷大")。
完整代码(Python3)
class Solution: def minSkips(self, dists: List[int], s: int, h: int) -> int: n = len(dists) dp = [[s * h + 1] * (n + 1) for i in range(n + 1)] dp[0][0] = 0 for i in range(1, n + 1): cur = dists[i - 1] for j in range(i + 1): # rest:向上取整到下一个整数小时(整数化后等价于 math.ceil) dp[i][j] = (dp[i - 1][j] + cur + s - 1) // s * s # no rest:跳过本次休息,直接累加真实耗时 if j > 0: dp[i][j] = min(dp[i][j], dp[i - 1][j - 1] + cur) # 提前返回:j 从小到大枚举,首次满足即为最小跳过次数 if dp[-1][j] <= h * s: return j return -1逐行要点:
dp = [[s * h + 1] * (n + 1) for i in range(n + 1)]:二维表初始化,s * h + 1充当"无穷大";- 内层
j从 0 到i:严格限制j <= i,保证状态合法; dp[i][j] = (dp[i-1][j] + cur + s - 1) // s * s恒先执行,保证j == 0时只有"休息"分支可用;if dp[-1][j] <= h * s: return j:在内层循环中同步判断,一旦发现当前j已能让最后一条路(dp[-1]即dp[n])准时抵达,立即返回——这正是"最小跳过次数"的贪心式提取,无需等整张表算完。
复杂度分析
令n为数组长度:
- 时间复杂度:
O(n^2)—— 双层循环,外层遍历 n 条路,内层枚举 0..i 共n(n+1)/2个状态; - 空间复杂度:
O(n^2)—— 二维dp表(可进一步优化为滚动数组压到O(n),感兴趣的读者可自行尝试,注意j的逆序枚举以保证转移依赖的dp[i-1][j-1]未被覆盖)。
复杂度与仓库 动态规划专题 中"DP 的时间空间复杂度打底就是状态总数(参数取值范围的笛卡尔积)"的论断完全吻合:本题状态数为
n * n量级,故复杂度为O(n^2)。
关键点总结
- 识别范式:带"跳过/不跳过"二元决策 + 最少次数求值 → 动态规划;先排除可行性二分,因为组合分布 $C_n^r$ 使判定不可行;
- 状态定义:
dp[i][j]= 到达第 i 条路终点且已休息 j 次所需时间,把"不确定的分布"收敛为第二个状态维度; - 转移二元性:休息分支做向上取整(
(cur + s - 1) // s * s),不休息分支直接累加,两者取min; - 边界纪律:
dp[0][0] = 0,j从 0 到i,j == 0禁止不休息分支; - 浮点精度:把全部时间乘以
speed化小数为整数,用整数取整代替math.ceil,彻底规避0.333…累加产生的1.0000000…2型误差; - 提前返回:从小到大枚举
j,首次满足dp[n][j] <= hoursBefore即返回,保证答案最小。
延伸阅读
- 本题完整题解:problems/5775.minimum-skips-to-arrive-at-meeting-on-time.md
- 动态规划方法论(状态定义、转移方程、枚举状态三件套):thinkings/dynamic-programming.md
- 能力检测二分范式与模板:91/binary-search.md
- 更多取整/精度类题目可对照 problems/875.koko-eating-bananas.md(同样使用
(pile + mid - 1) // mid的整数取整技巧)
【免费下载链接】leetcodeLeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解,记录自己的leetcode解题之路。)项目地址: https://gitcode.com/gh_mirrors/le/leetcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考