前两天有个朋友拿了一道“老鼠出动顺序”的题来问我,说题目特别短,但他第一反应是去模拟两只老鼠的追尾过程,写着写着就乱了。我拿 Python 配合单调栈手推了几组数据之后,发现这题非常适合用来演示单调栈的一种常见形态:相邻元素互相淘汰,栈内始终维持某种单调性。很多人对单调栈的印象停留在“下一个更大元素”“柱状图最大矩形”,但实际上算法题只要出现“后面的东西会吃掉前面的东西”“先完成的候选会取代之前的候选”这类逻辑,都能往单调栈上靠。
这道题的核心场景可以这样理解:洞口在坐标 0,所有老鼠都在正半轴,存在一条只能前后排列的单向通道。每只老鼠有初始位置和奔跑速度,统一朝洞口方向跑。如果后面的老鼠速度足够快,能在前面那只老鼠到达洞口之前追上它,就会把前面的老鼠“吞并”,然后保持自己的速度继续跑。最终从洞口依次出来的不是所有初始老鼠,而是一串“赢家”,题目要求你就按这种顺序输出它们。适合看这篇内容的人,是想把单调栈真正用起来的算法学习者,或者准备笔试面试、想快速搞懂这类追及问题背后公式推导的开发者。
正文我会先把题意翻译成模型,再推导关键不等式,然后给出可复制的 Python 实现,最后把我在实际调试中踩过的坑、以及这个模型的常见变体一起梳理一遍。
1. 先把这个“追及题”翻译成算法题
1.1 规则没有你想的那么复杂
先把物理场景固定下来。洞口在坐标 0 的位置,老鼠在正半轴不同位置,也就是离洞口有远有近。所有老鼠同时开始朝 0 这个方向跑。注意通道是单行的,后面的老鼠不可能超车到前面去,只能追尾。如果后鼠在“前鼠到达洞口之前”追上了前鼠,就把前鼠吞并掉,自己继续按原来的速度往洞口跑。这里有一个很多人第一反应会搞错的规则:吞并之后速度并不变慢,也不会变成前后排队一起慢慢跑,而是后鼠保持自己的速度继续前行。
为什么题目会这样设定?因为如果合体后取两只老鼠中较慢的速度,那么最靠近洞口的老鼠一定最先到达,后面的老鼠不管多快都会被前面慢的堵住,最终所有老鼠出洞顺序就是它们初始位置的先后顺序,没有任何算法含量。所以这道题能做成单调栈,核心前提就是后面的快鼠吞并前鼠后不减速,通俗讲就是“谁快谁赢,快的还能继续快”。
当题目给你 n 只老鼠的初始坐标 x 和速度 v 时,你要回答的并不是它们实际在哪个点追上,而是最后那串出洞事件。比如:
- 老鼠 A 在坐标 2,速度 1,单独到洞口需要 2 秒;
- 老鼠 B 在坐标 3,速度 3,单独到洞口需要 1 秒。
B 能追上 A 吗?初始距离是 1,相对速度是 2,所以 0.5 秒后就追到了。追上时离洞口还有 1.5,B 以速度 3 继续跑,总用时刚好是 3 除以 3 等于 1 秒。也就是说 A 虽然更靠近洞口,却会在半路被吞掉,最终第一个出洞的反而是更远、更快的 B。这道题反直觉的点就在这里:离洞口近不代表能活到最后,关键要看谁先到洞口。
1.2 为什么直接模拟两两追尾不划算
我最初看到这题时,第一反应是按时间轴做事件模拟:每次找出下一次追尾发生的时间和位置,合并两只老鼠,然后继续找下一组。这种思路在 n 很小的时候可行,但复杂度很容易写成 O(n^2) 甚至更差,因为每次合并之后,老鼠之间的相对距离、追尾时间都要重新算一遍。
更麻烦的是,一次追尾可能引发连锁反应。某只快鼠在追上最近的前鼠之后,并不代表它到洞口了,如果前面还有更慢的老鼠,它可能继续追、继续吞。这种“一连串淘汰”和单调栈的“不断弹出栈顶直到满足条件”天然相似。所以这道题的标志性解法就是用一遍单调栈扫描,而不是真的在时间轴上模拟。
如果你已经接触过“行星碰撞”类题目,会发现精神很像:序列中有些元素会被新来的元素触发移除,移除之后还要继续和新的栈顶比较。区别在于行星碰撞看的是符号方向,这里看的是“独立到洞时间”的大小关系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心推导:让问题变简单的关键不等式
2.1 每只老鼠的“独立到洞时间”
每一只老鼠如果单独朝洞口跑,没有任何阻挡,到达洞口的时间是:
text复制t = x / v
这个时间我叫它“独立到洞时间”。题目总喜欢让我们直观理解为:只要后鼠速度比前鼠快,就可能追上。但这只是一个必要条件,并不充分,因为它还与两只老鼠的初始距离有关。真正等价于“后鼠能在前鼠到洞前追上”的条件,不是 velocity 大,而是后鼠的独立到洞时间小于前鼠的独立到洞时间。
这个结论可以用一个比较直观的方式来记忆:把路程看成“距离洞口还剩多远”,速度看成“每秒减少这段距离的速度”,那么后鼠想要在到达洞口前碰触前鼠,本质上是它“清空自己到洞口这段距离”的耗时更短。我实际手推过公式,假设后鼠坐标 x2,速度 v2,前鼠坐标 x1,速度 v1,x1 小于 x2,追及时间等于:
text复制(x2 - x1) / (v2 - v1)
前鼠到洞口的时间是 x1 / v1。让追及时间小于前鼠到洞时间,最终可以化简成:
text复制x2 / v2 < x
