《编程之美》4.7节描述了蚂蚁爬杆问题,把所有具体数字都表示成字母后变为形如如下形式的问题:
有一根长为\(L\)的平行于x轴的细木杆,其左端点的x坐标为0(故右端点的x坐标为\(L\))。刚开始时,上面有\(N\)只蚂蚁,第\(i ( 1\le i\le N ) \)只蚂蚁的横坐标为\(x_i\)(假设\(x_i\)已经按照递增顺序排列),方向为\(d_i\)(0表示向左,1表示向右),每个蚂蚁都以速度\(v\)向前走,当任意两只蚂蚁碰头时,它们会同时调头朝相反方向走,速度不变。编写程序求所有蚂蚁都离开木杆需要多长时间。
该问题是经典问题了,有\(O(N)\)的解法。昨天和赵牛同学讨论了该问题的一些扩展,赵牛均给出了精妙解答,现列出如下:
- 从左边数起的第\(i\)只蚂蚁什么时候走出木杆?
- 所有蚂蚁从一开始到全部离开木杆共碰撞了多少次?
- 第\(k\)次碰撞发生在哪个时刻?哪个位置?哪两个蚂蚁之间?
- 哪只蚂蚁的碰撞次数最多?
- 如果不是一根木杆而是一个铁圈,经过一段时间后所有蚂蚁都会回到的状态吗?这个时间的上界是多少?
扩展1的解答
现在来解决扩展1。这个解答甚是精妙,通俗点来说,我们假设每只蚂蚁都背着一袋粮食,任意两只蚂蚁碰头时交换各自的粮食然后调头。这种情况下,每次有一只蚂蚁离开木杆都意味着一袋粮食离开木杆(虽然可能已经不是它刚开始时背的那一袋了)。于是,我们可以求出每袋粮食离开木杆的时间(因为粮食是不会调头的)。又由于每袋粮食离开木杆的时间都对应某只蚂蚁离开木杆的时间,这是一种一一映射关系。现在我们要找到对应于第\(i\)只蚂蚁的那个映射。在此之前需要证明一个命题:
若一开始时有\(M\)只蚂蚁向左走,\(N-M\)只蚂蚁向右走,则最终会有\(M\)只蚂蚁从木杆左边落下,\(N-M\)只蚂蚁从木杆右边落下。
这个命题很容易证明:每次碰撞均不改变向左和向右的蚂蚁数量。于是,由于每次碰撞蚂蚁都会调头而不是穿过,最后必定是从左边数起的前\(M\)只蚂蚁从左边落下,后\(N-M\)只蚂蚁从木杆右边落下。由于我们知道每袋粮食是从哪边落下的,故左边落下的\(M\)袋粮食的离开木杆的时间就对应于从左边数起的前\(M\)只蚂蚁离开木杆的时间,右边的类似。因此,我们只需判断\(i\)和\(M\)的关系,便知道第\(i\)只蚂蚁是从左边还是右边落下。不妨假设是从左边落下,因此该蚂蚁落下的时间就等于从左边落下的第\(i\)袋粮食的落下时间。时间复杂度\(O(N)\),一遍扫描搞定。
扩展2的解答
对于扩展2,我们只需求得每个蚂蚁的碰撞次数,然后累加即可。在这里我们换一种思路,把碰撞调头看成不调头而继续向前(穿过),则容易看出原问题(碰撞次数)就变为求穿过的次数(因为速度大小不变,原来的每次碰撞都对应于现在的一次穿过)。则对于每只向左的蚂蚁,它只会“穿过”那些在它左边的向右走的蚂蚁。因此,每只蚂蚁“穿过”的蚂蚁次数等于刚开始时在它前进方向上与它前进方向相反的蚂蚁个数。时间复杂度也是\(O(N)\)。改为用粮食的观点来理解也是可以的。
扩展3的解答
第3个扩展问题有点复杂。首先我们假设\(v\)为0.5个单位长度每秒,每个蚂蚁刚开始时都处于整点处,这样每次碰撞都发生在整秒处。这个假设有个好处,就是我们可以二分第\(k\)次碰撞的时刻。如果碰撞时刻是浮点数,这个二分有可能永远不会终止。我们还是看成每个蚂蚁驮着一袋粮食,那么每袋粮食易主(即从一个蚂蚁身上交换到另一个蚂蚁身上)时,就发生了一次碰撞。由于粮食的方向是固定不变的,我们可以很容易求出每一袋粮食在它的“前进”方向上的所有易主时刻(它易主的次数等于扩展2中的“穿过”次数)。这样,我们的问题就等价于:
找到最小的时间\(t\),使得易主时刻小于或等于\(t\)的易主次数大于或等于\(k\)。
由于现在所有碰撞(易主)的时刻都是整点,故我们可以二分\(t\),然后用线性复杂度找出易主时刻小于或等于\(t\)的易主次数。整个复杂度为\(O(N*log(|t_{max}-t_{min}|)\),其中\(t_{max}\)和\(t_{min}\)分别表示第一次和最后一次碰撞的时刻,均可在\(O(N)\)时间内求出。
在上一段中,要想使用线性时间复杂度求出易主时刻小于或等于\(t\)的易主次数还需要一点技巧。可以这样:用一个数组\(p_i\)表示第\(i\)个向右走的蚂蚁的初始位置,当扫描到第\(j\)个向左走的蚂蚁时,假设得到的中值点为\(i’\)(即\(p_0\)到第\(p_{i’}\)个位置上对应的粮食和该袋粮食的易主时刻均大于\(t\))。由于该袋粮食向左易主的时刻是递增的,而下一个向左走的蚂蚁的初始位置又大于当前(第\(j\)个向左走的)蚂蚁,故对于下一个蚂蚁ant来说,\(p_0\)到第\(p_{i’}\)个位置上对应的粮食和ant所驮粮食的易主时刻也一定大于\(t\)。即中值点的位置是单调的。因此可以在均摊\(O(N)\)的时间内算出所求个数。
求出时刻的同时我们也求出了位置,故第二小问也解决。接下来要求哪两个蚂蚁发生了这次碰撞(如果同时存在多个碰撞求出任意一个即可)。其实,我们只需要求出每袋粮食在\(t\)时刻的位置即可。因为每袋粮食必然对应于一个驮着它的蚂蚁,故我们只需对这些粮食的位置排序,找出位置相同的粮食以及其下标(即从左到右第几个),也就找出了那对蚂蚁了。
扩展4的解答
对于第4个扩展,只要求出每只蚂蚁的碰撞次数即可。这也解决了扩展2的解答中原始思路。首先由扩展1的解答我们可以知道每只蚂蚁最终是往左还是右掉下去,然后假设第\(i\)只蚂蚁最终往左掉下,而开始时刻其左边有\(r\)只向右走的蚂蚁,则它至少要朝左边碰撞\(r\)次才能把左边的蚂蚁全撞成向左的状态。倘若它一开始就是向左的,则共要碰撞\(2r\)次,否则为\(2r+1\)次。这样利用一个数组和几个计数器仍能在\(O(N)\)时间内求出每个蚂蚁的碰撞次数,取最大那个即可。
扩展5的解答
这个问题看起来挺复杂,其实很简单。假设环长为\(L\),则一个蚂蚁走完一圈需要\(T=L/v\)的时间。首先,还是像上面的讨论那样假设每个蚂蚁都驮着一袋粮食。那么,经过\(T\)时间后所有粮食都回到了原来的位置。由于每袋粮食都对应一个蚂蚁,而蚂蚁每次碰撞都会调头,因此蚂蚁的相对位置是不变的,这就说明经过\(T\)时间后蚂蚁循环移动了。假设移动了\(s\)个位置,即每个蚂蚁都到达它往右第\(s\)个蚂蚁的初始位置,那么,类似地,再经过\(T\)时间,当前状态仍会循环移动\(s\)个位置。容易看出这是一个最小公倍数问题:循环移动多少个\(s\)次之后每个蚂蚁回到自己位置?答案为\(LCM(N,s)/s\),于是最多经过\(T_{max}=LCM(N,s)/s*T \le N*T\)时间,每个蚂蚁都至少回到原地一次。
除了以上几个扩展,还有一些个人认为比较变态的扩展,有的没空仔细想,有的暂时没想到解法,也列出如下,欢迎拍砖:
- 如果每只蚂蚁的速度不一样(这就有可能由于追赶而产生碰撞,此时根据动量守恒定律:(,速度互换),上述扩展问题的答案是什么呢?
- 如果蚂蚁在一个平面上运动,同样也是碰头后原路返回(注意这不等同于两只蚂蚁交换继续前进),问是否所有蚂蚁都能最终离开平面?
- 在上述情况下,如果最终能离开平面,离开平面需要多长时间?
- 在上述情况下,回答关于一维的前文讨论的每个问题。
另外,赵牛同学又提出了一些更bt的扩展,如下:
- 假设每个蚂蚁都有重量,两只蚂蚁碰撞时轻的那个有一定几率从旁边被撞下去:(,那又该怎样?
- 假设不是被撞下去而是有一定几率被撞晕而停滞几秒,那又该怎样?
blablabla…