ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

龟兔赛跑预测进阶:状态机模拟与边界条件全解析

龟兔赛跑预测进阶:状态机模拟与边界条件全解析 我最早接触“龟兔赛跑预测”这道题是在大学OJ的训练列表里那时候它还是入门级的模拟题规则很简单兔子偷懒睡觉乌龟拼命爬比谁先到终点。后来在进阶题里再次碰到它也就是这次的“进阶题6”我才发现这道题远没有看起来那么人畜无害——它把速度、休息时间、总路程这些参数全部放开要求的不再是简单比大小而是精确预测整个比赛过程中每一刻的状态变化。很多人在这一步栽了跟头代码跑出来的结果不是差一分钟就是差一个名次查半天都找不到问题。这篇文章就围绕这个经典题目展开重点说清楚状态机的设计、边界条件的处理、两种主流写法的优缺点以及我在调试过程中踩过的一些坑。不管你是刚接触算法题的初学者还是已经刷过不少模拟题想回头查漏补缺的选手这篇文章都值得花十分钟读完。1. 先别急着写代码龟兔赛跑到底在预测什么1.1 三个隐含前提漏掉一个就全错无论题目描述怎么包装龟兔赛跑预测的核心规则几乎固定兔子速度比乌龟快但兔子跑一会儿就会休息乌龟速度慢但从来不停。表面上看这是一道“谁先到终点”的判断题但实际上它要求你回答的是整个比赛过程中任意时刻双方的位置关系。这背后藏着三个隐含前提很多人写挂都是因为漏掉了它们。第一速度是恒定的。兔子不会因为睡醒之后加快脚步乌龟也不会因为追平了兔子就减速全程都是匀速直线运动。这一点看起来简单但直接影响后面的计算方式——如果速度可变那就得用积分或者微分方程复杂度立刻上一个台阶。第二总路程是固定的。所有判断都必须以“到达终点”为终止条件不能因为兔子领先了十倍路程就提前宣布比赛结束也不能因为兔子正在睡觉就认为它会输。一切以位置和时间说话。第三兔子的休息是“按时段”的不是“按距离”的。兔子每跑满一定时间就会睡固定时长睡醒之后继续跑再跑满同样时间又睡。这个循环可能一直持续到比赛结束也可能在途中被打断——因为兔子可能睡到一半乌龟就已经爬过终点了。这三个前提构成了题目的底层框架。理解它们之后你会发现这本质上不是一个数学题而是一个模拟题你需要把时间一步一步往前推看每一刻双方的位置各自在哪里什么时候有人越过终点线比赛就什么时候结束。1.2 为什么“平均速度”思路在这里是坑我第一次做这类题的时候第一反应是算平均值兔子虽然跑得快但中途要睡那不如把总睡觉时间算出来然后用总路程除以实际花的总时间得到一个“等效速度”再和乌龟的速度比一比。这个思路在部分场景下能跑通但一旦进入进阶版就会翻车。原因很简单兔子的睡觉时间不是固定的它取决于兔子到底跑了多久。如果兔子跑得非常快可能跑个十几分钟就冲到终点了比赛已经结束后面根本不存在睡觉时间。换句话说睡觉时长是一个依赖执行路径的结果不是一个先验常量。就算你枚举兔子可能的睡眠次数也得先确定兔子到底跑了多少个“跑-睡”周期而这个周期数又依赖于总路程和兔子速度的比例关系。算着算着你会发现这已经不小心变成了分类讨论问题兔子能不能在第一次睡觉前到终点兔子能不能在睡醒后第二次冲刺时到终点兔子会不会被乌龟反超分类讨论不是不能做但条件一多就很容易漏。特别是那种“兔子虽然早就到了终点但乌龟还在半路”的情况很多人会把终点后的时间也算进睡觉时长里导致最终输出时间和正确答案差出一截。所以与其硬推公式不如老老实实写一个状态机去模拟整个过程。这个方法笨是笨了点但它逻辑清晰、不容易漏条件而且更容易排查问题。2. 状态机设计把比赛拆成离散的瞬间2.1 兔子的三种状态必须用变量锁死既然决定走模拟路线第一步就是给兔子定义状态。表面上看兔子只有“跑”和“睡”两种状态但实际写代码的时候我习惯拆成三种跑步中、睡觉中、已到达终点。为什么要把“已到达终点”单独拎出来因为这三种状态下兔子的位置更新逻辑完全不同。跑步中每过一个时间单位兔子的位置就增加v1。 睡觉中位置不变但累计睡眠时间增加。 已到达终点位置不再更新而且也不再参与任何状态切换判断。很多人只定义了前两种状态结果兔子到终点之后还在“睡觉”累计睡眠时间还在涨虽然没有影响最终输出但会让调试过程变得很别扭而且一旦你后面加了“比赛结束时输出兔子状态”之类的功能就会埋雷。乌龟的状态就简单了从头到尾只有一个“爬行中”每过一个时间单位位置增加v2。当然从严谨角度来说乌龟也有“已到达终点”的状态但在我们的模拟框架里一旦有人到达终点比赛就结束了所以乌龟到终点之后循环就退出不需要额外处理。这里有一个很容易被忽略的细节兔子跑步的时长也是一个累计值。不是说兔子处于“跑步中”状态就永远跑下去而是每跑满T秒或分钟具体看题目单位就要切换到睡觉状态同时累计跑量清零睡眠时长归零。如果你用if判断“当前状态是跑步中且连续跑了超过T秒”必须记得在切换状态的那一刻重置计时器否则下一轮判断就会出错。我用一个简单的表格总结一下状态切换的触发条件和操作当前状态触发条件下一状态需要重置的变量跑步中连续奔跑时长达到T睡觉中奔跑累计时长清0睡觉中连续睡眠时长达到R跑步中睡眠累计时长清0跑步中位置到达或超过总路程D已到达无睡觉中位置到达或超过总路程D已到达无核心判断就四个分支看起来没什么了不起的但实际写起来非常容易乱尤其是“跑步中”和“睡觉中”都要检查“是否到终点”这一点漏掉任何一个都会让程序在特殊数据下跑出错误答案。2.2 最关键的逻辑睡觉时被乌龟追上怎么办兔子的速度远快于乌龟所以在兔子清醒的时候它一定是领先的。问题出现在兔子睡觉的阶段兔子位置原地不动乌龟却在一刻不停地往前爬乌龟会不会在兔子睡觉期间追上兔子甚至反超答案是当然会。很多人的代码在这一点上处理得很粗糙直接让兔子一觉睡到自然醒然后再比较双方位置。这在时间颗粒度比较大的场景下没问题但如果你写的是按“分钟”或按“秒”推进的模拟就会遇到一个哲学问题兔子在睡眠中的第几秒被乌龟追上的这个瞬间需不需要单独处理其实不需要。我们的模拟目标不是还原每一秒的动态赛况而是找出“谁先到达终点”以及“到达时间是多少”。兔子睡觉期间乌龟爬过的路就是那几分钟内的路程增量只要在每次时间步结束时比较双方位置就能知道乌龟是否追平或反超。至于是在睡眠的第3秒追上的还是第7秒追上的对最终答案没有影响。不过有一种特殊情况确实需要脑子清醒一下如果乌龟在兔子睡觉期间超过了兔子但兔子在睡醒之后又凭借速度优势反超回来那么比赛过程中是先乌龟领先、再兔子领先还是兔子一路领先从未被超如果题目要求输出“比赛过程中谁曾经领先过”那你就必须记录每个时刻的位置关系变化而不能只看终点时的状态。好在“进阶题6”的常见要求只有两个输出谁先到达终点以及到达的时间。如果题目要求的是“预测比赛结果”而不是“描述过程”那确实不需要追踪领先权的交替。但我也见过一些变体题目会额外要求输出“兔子醒来时谁领先”这种就得多写一个判断了。3. 实操环节两套可复现的代码方案3.1 时间推进法最直观但要注意性能时间推进法的思路很简单从第0分钟开始每分钟检查一次双方位置更新状态直到有人到达终点。我先把C版本的核心代码写出来这个版本比较接近大多数教科书上的写法适合初学者理解。#include iostream using namespace std; int main() { long long v1, v2, T, R, D; cin v1 v2 T R D; long long rabbit_pos 0, turtle_pos 0; long long time 0; long long run_time 0, sleep_time 0; while (rabbit_pos D turtle_pos D) { // 乌龟一直在爬 turtle_pos v2; // 兔子状态机 if (rabbit_pos D) { // 兔子已经到终点不再更新位置 } else if (sleep_time 0) { // 还在睡觉 sleep_time--; if (sleep_time 0) { // 睡醒了下一轮开始跑 // 这里不用额外操作下一轮进入跑步分支 } } else { // 跑步状态 rabbit_pos v1; run_time; if (run_time T) { // 跑满T分钟准备睡觉 sleep_time R; run_time 0; } } time; } // 输出结果 if (rabbit_pos D turtle_pos D) { cout D endl; } else if (rabbit_pos D) { cout R endl; } else { cout T endl; } cout time endl; return 0; }这个写法有个不好的地方把跑步状态的判断放在else里实际上要求兔子的状态判定按照“是否睡觉”优先。也就是说只要sleep_time 0哪怕兔子上一轮刚刚到终点也还是会进入睡觉分支只不过我在前面加了一个if (rabbit_pos D)的短路判断。这种做法能跑但逻辑上有点绕不太建议长期使用。更好的写法是把兔子的状态定义成显式变量再写一个状态机更新函数。下面这个版本结构更清晰#include iostream using namespace std; int main() { long long v1, v2, T, R, D; cin v1 v2 T R D; long long rabbit_pos 0, turtle_pos 0; long long time 0; // status: 0run, 1sleep, 2finished int rabbit_status 0; long long state_counter 0; while (rabbit_pos D turtle_pos D) { turtle_pos v2; if (rabbit_status 0) { rabbit_pos v1; state_counter; if (state_counter T) { rabbit_status 1; state_counter 0; } } else if (rabbit_status 1) { state_counter; if (state_counter R) { rabbit_status 0; state_counter 0; } } if (rabbit_pos D) { rabbit_status 2; } time; } if (rabbit_pos D turtle_pos D) { cout D endl; } else if (rabbit_pos D) { cout R endl; } else { cout T endl; } cout time endl; return 0; }这个版本用rabbit_status显式标识状态state_counter当作计时器。每次进入跑步状态就累加累加到T就切换进入睡觉状态也累加累加到R就切换。等兔子位置超过终点把状态改成2之后循环自然满足退出条件。时间推进法最怕的是时间单位太小。如果题目给定的速度单位是“米/秒”而路程单位是“米”一秒一秒地推没什么压力。但如果路程很大、速度很慢需要几万秒才能跑完每秒推一次也还行现代CPU完全没有压力。真正的问题是如果题目允许小数时间比如0.1秒更新一次那你得把时间步长缩小10倍循环次数暴涨10倍此时就要考虑事件驱动法了。3.2 事件驱动法分段计算更快也更优雅事件驱动法的核心思想是不要每秒推一次而是找出所有可能改变兔子状态的“关键时间点”在这些时间点之间用公式一次性算完。关键时间点只有三类兔子每次睡醒、开始跑步的时刻。兔子每次跑满T时间、准备睡觉的时刻。任意一方到达终点的时刻。这些时间点之间的距离要么是兔子在匀速跑要么是兔子在睡觉两段之间的运动规律都非常简单可以直接用公式算位置。我用Python写一个事件驱动版因为Python的while循环和元组处理在这种场景下比较顺手。v1, v2, T, R, D map(int, input().split()) rabbit_pos 0 turtle_pos 0 current_time 0 # 兔子状态: running or sleeping state running # 当前状态的剩余时间 state_remaining T # 初始先跑T分钟 while rabbit_pos D and turtle_pos D: if state running: # 兔子还能跑多久有两种可能跑满T分钟或者有人先到终点 time_to_finish min( state_remaining, (D - rabbit_pos v1 - 1) // v1 if rabbit_pos D else 0, (D - turtle_pos v2 - 1) // v2 if turtle_pos D else 0 ) # 但这步写复杂了实际可以简化 # 简化先假设兔子不会中途到终点专注处理“跑满T”的场景 if state_remaining (D - rabbit_pos v1 - 1) // v1: # 兔子跑满状态剩余时间进入睡觉 current_time state_remaining rabbit_pos v1 * state_remaining turtle_pos v2 * state_remaining state sleeping state_remaining R else: # 兔子先到终点比赛结束 needed_time (D - rabbit_pos v1 - 1) // v1 current_time needed_time rabbit_pos v1 * needed_time turtle_pos v2 * needed_time break else: # sleeping # 兔子睡觉乌龟可能追上甚至到终点 time_to_turtle_finish (D - turtle_pos v2 - 1) // v2 if state_remaining time_to_turtle_finish: # 乌龟在兔子睡醒前到不了终点兔子睡到自然醒 current_time state_remaining turtle_pos v2 * state_remaining state running state_remaining T else: # 乌龟先到终点比赛结束 current_time time_to_turtle_finish turtle_pos v2 * time_to_turtle_finish rabbit_pos 0 break # 判断胜负 if rabbit_pos D and turtle_pos D: print(D) elif rabbit_pos D: print(R) else: print(T) print(current_time)这个版本我没有把所有边界情况都处理干净比如睡觉期间兔子位置不变但乌龟到终点时兔子位置肯定没到终点所以胜负判断里rabbit_pos D为假再比如兔子在跑步期间和乌龟同时到终点这种需要用而不是。事件驱动法的好处是循环次数极少基本就是兔子的“跑-睡”周期数哪怕数据范围扩大到10的9次方级别也毫无压力。缺点就是边界条件比时间推进法更繁琐需要你非常清楚“在哪个事件点检查谁先到达终点”这个问题。如果你只是追求通过这道题我建议先用时间推进法把正确答案写出来再用事件驱动法做性能优化。不要一上来就挑战高难度写法否则排查问题的时间可能是写正确代码本身的好几倍。3.3 测试用例怎么设计才能覆盖全很多人在OJ上提交失败不是因为算法错了而是因为没想全测试场景。我总结了一组测试数据基本能把这道题的所有坑都踩一遍。第一组兔子不睡觉直接到终点。比如v110, v21, T100, R100, D50兔子5分钟就到了乌龟才走了5米。这组数据适合验证基本逻辑答案应该是兔子胜时间5分钟。第二组兔子睡醒之后依然遥遥领先。比如v110, v21, T2, R1, D100兔子跑2分钟睡1分钟醒后继续跑。算一下跑2分钟20米睡1分钟再跑2分钟40米睡1分钟……兔子到达100米要跑10个2分钟中间睡9次因为最后一次跑完已经到终点了不需要再睡总时间10*2 9*1 29分钟。乌龟29分钟只跑29米远远落后。这道题的正确输出是兔子胜时间29。第三组乌龟在兔子睡觉期间反超并获胜。设v15, v23, T1, R2, D10。兔子第1分钟跑5米睡2分钟此时乌龟已经跑了3分钟位置9米。兔子第4分钟醒来开始跑此时兔子5米乌龟已经9米了。兔子跑1分钟到10米乌龟同时从9米爬到12米兔子到终点乌龟虽然也过了但时间上兔子先到不对要仔细算第1分钟结束兔子5米乌龟3米。第2分钟兔子睡觉乌龟到6米。第3分钟兔子睡觉乌龟到9米。第4分钟兔子醒来跑1分钟位置10米到达终点同时乌龟从9米到12米也过了终点。但判断顺序是谁先跨过终点在第4分钟这1秒内兔子从5米跑到10米乌龟从9米爬到12米。兔子在第4分钟的末尾到达10米乌龟在第4分钟开始时就差1米大约在第4分钟的第0.33分钟就到了12米不对乌龟第4分钟时位置是9米它以速度3米/分只需要1/3分钟就到达10米也就是说乌龟先到达终点。那这组数据答案是乌龟胜时间约3.33分钟而不是整数。如果题目只给整数输出说明这个测试用例设计得不合适但实际题目数据一般会保证时间是整数或者要求输出浮点数。这提醒一个重要问题如果时间不是整数你的模拟粒度就很重要。按整数时间推进时最后一小段可能凑不整这时候要分四种情况兔子已经到了、乌龟已经到了、同时到、都没到。为了保险我在时间推进法中加了一个“如果已经到终点就break”的判断就是因为最后一轮的位置增量可能超出终点长度导致多算一格时间。第四组双方同时到达。比如v110, v25, T100, R100, D100兔子10分钟到乌龟20分钟到肯定不同时。要构造同时到达的数据得让兔子睡觉时间刚好补上速度差理论上乌龟到达总时间 D/v2兔子实际跑步时间 D/v1兔子睡眠总时长 D/v2 - D/v1而且这个总时长必须是若干次完整休息时长的和。这种数据比较难手搓但从题目角度来说这种边界情况是判题系统非常喜欢出的因为同时到达的输出约定——到底是D还是R取决于题目具体定义。有的题目约定输出“平局”有的约定输出“兔子胜因为乌龟先爬过终点线前兔子已经在线前”这些都要看原题描述。所以我不建议自己手算同时到达的输入不如直接从题库或者随机生成器里构造。重点是你的代码一定要用来判断终点到达而不是否则在浮点误差或整除场景下会漏判。4. 常见问题与调试实录这些坑我全都踩过4.1 死循环状态切换忘记重置计时器新手最容易触发的bug就是死循环。典型表现是兔子进入睡觉状态后state_counter一直在累加但兔子永远不会从睡眠切换回跑步因为你在判断睡眠结束的条件里写着if (state_counter R)但 R 可能是0。如果 R 等于0意味着兔子不睡觉。很多题目的数据范围里R确实可以取0。这时候你的状态机应该直接跳过睡觉状态而不是先切到睡觉状态再秒切回跑步。如果代码逻辑是“状态变为sleepingstate_counter加1下一次判断是否等于0”那没问题但如果你写的是“先等state_counter加到R再切换”R0时就会永远卡在睡觉状态因为第一次进入睡觉分支时就得立刻切换而不会等到下一轮。解决这个问题的方法很简单切换状态时同步更新state_counter。如果 R0在设置状态的同一行就把下一次跑步状态准备好。4.2 差一分钟终点判断的边界处理另一个高发bug是最终输出时间差1。比如兔子在第5分钟刚好到达终点但你的循环在第6分钟才判断到rabbit_pos D于是输出6而不是5。为什么会出现这种情况因为你的循环逻辑是“先移动位置再判断是否到达”还是“先判断是否到达再移动位置”如果是前者那兔子在第5分钟移动到了终点但循环还没结束第6分钟继续移动位置超过终点输出时间就多了1。正确的做法是每轮循环开始时检查双方是否已经到达如果到达直接退出并输出当前时间。如果坚持在更新位置后再判断则必须允许break发生在更新后且不能再推进时间。我在前面那段C代码里就是先更新位置再time如果位置在time之后才检查就会出问题。所以我把判断放在最后并且time放在break之后不对仔细看代码我的逻辑是time在每轮末尾位置更新在每轮开头那么位置到达终点时这轮的time值还没有递增下一轮开头判断while条件时发现不满足就退出了但这会丢失一次循环内更新位置后的时间标记。为了讲清楚我画一个手写流程图在脑子里过一遍第5分钟开始时time4假设初始0更新位置兔子到终点。循环末尾 time 变成5。第6分钟开始时while条件 rabbit_pos D退出循环。输出time5。这其实是对的。但如果你在位置更新后立刻判断并break而不是走到循环末尾那time还是4漏加1。这就是差一分钟的来源。看起来只是代码逻辑小差异实际运行结果天差地别。调试技巧在关键位置加输出打印每分钟的兔子位置、乌龟位置、当前时间单步跟踪一小段就能定位问题。4.3 胜负输出判断顺序很多人在最终判断时先判断兔子是否到达再判断乌龟是否到达这导致同时到达时输出兔子胜和题目要求不一致。正确做法是先判断是否同时到达再判断单方面到达。顺序不能反。if (rabbit_pos D turtle_pos D) { cout D endl; } else if (rabbit_pos D) { cout R endl; } else { cout T endl; }这段代码在同时到达时输出D也就是平局。如果题目对平局有特殊输出要求比如“R”或“T”你只需要调整这个优先顺序就行。但我强烈建议先把平局情况单独拎出来处理避免和“兔子领先到达”“乌龟领先到达”混在一起。4.4 数据范围用long long别用int最后一个不起眼但影响巨大的问题数据范围。v1和v2通常不会给得太离谱比如1到100之间。但D可能给到10的9次方R和T也可能给到大数。如果你用int存这个距离一旦超过21亿多就会溢出变成负数然后你发现兔子跑了半天位置是负的比赛永远结束不了。在竞赛环境里这种题目输入范围一般都会卡在int能表示的范围边缘用long long是最稳妥的。就算某些题目测试数据不大用long long也没有性能损失无非多占几个字节换来的是安心。我还遇到过一种情况速度单位是米/分钟但路程单位是千米需要在输入后统一换算。这种单位混用的问题在工程场景里更常见但在OJ里偶尔也会变着花样出。所以我每次写完模拟题都会手算几组小数据去验算单位是否一致。5. 从这道题延伸出去状态机模拟的通用方法论5.1 凡是“一个动一个停”的问题都是状态机问题龟兔赛跑预测只是状态机模拟的一个缩影。你仔细想想电梯调度、收费站排队、红绿灯路口的车流模拟本质上都是“多个主体在不同状态下按时间推进更新位置或属性”的问题。状态机模拟的通用套路是三步第一定义状态。每个主体有哪些状态每个状态意味着什么行为。第二定义状态切换的触发条件以及切换时需重置哪些变量。第三定义时间推进的单位以及每个时间单位内各主体执行的操作。只要这三件事想清楚代码是水到渠成的事。很多人写状态机感到吃力不是代码能力弱而是状态定义得模棱两可切来切去就把自己绕晕了。5.2 用状态转移表替代一长串if-else当状态变多时一长串if-else会变得非常难维护。我在工程里习惯先画一个状态转移表然后用表驱动的方式写代码。以龟兔赛跑为例状态转移表就是我在前面列过的那张四行表。每一行列出了当前状态、触发条件、下一状态、以及需要重置的变量。写完表之后代码逻辑就变成了“根据当前状态查表执行对应的转移动作”。这样一来哪怕以后增加新状态比如“兔子脚扭了慢跑”也只需要在表里加一行代码主体几乎不用改。这就是规范工程方法在算法题中的降维应用。虽然题目本身不要求你代码写得多么工程化但养成这种习惯对你以后做更大的项目会帮助非常大。5.3 从模拟到预测一维坐标下的动态规划还有一个有意思的延伸方向如果题目要求的不是“谁先到终点”而是“给定任意时刻t预测兔子和乌龟的位置”那就变成了一个一维动态规划问题。你可以预处理出两个数组rabbit_pos[t]和turtle_pos[t]分别表示第t秒的位置然后直接索引查询。这种预处理在数据范围不超过10的6次方时非常高效如果t可以到10的9次方那就需要压缩状态或者用事件驱动法配合二分查找定位关键时间点。不管怎么变核心还是那一套状态定义、边界控制、时间推进。把基础打牢变化再多也只是换皮而已。6. 一点调试心得说实话这道题我重写过三版。第一版用平均速度估算直接WA第二版用时间推进法因为终点判断顺序不对差1分钟WA了一次第三版才把状态机梳理清楚一次性通过。我个人实际操作中的体会是遇到这种带有明确过程变化的题目不要急着推数学公式先写下“状态-转移-时间推进”三要素再动手写代码。磨刀不误砍柴工状态想清楚了代码就是翻译而已。还有一个小技巧调试的时候把每一步的time, rabbit_pos, turtle_pos, rabbit_status全部打出来哪怕打印几百行也比睁着眼睛瞎猜强得多。人脑不适合模拟电脑执行但你一旦把执行轨迹可视化错误往往一眼就能看出来。龟兔赛跑预测这道题本身不算难难的是把边界情况和状态切换处理干净。它就像一把卡尺量的是你对模拟类问题的基础功扎不扎实。把这道题吃透很多同类型的模拟题你都能顺势拿下。
返回列表