编程之美4.7 | 蚂蚁爬杆

有一根长为L的平行于x轴的细木杆,其左端点的x坐标为0(故右端点的x坐标为L)。刚开始时,上面有N只蚂蚁,第i(1≤i≤N)只蚂蚁的横坐标为xi(假设xi已经按照递增顺序排列),方向为di(0表示向左,1表示向右),每个蚂蚁都以速度v向前走,当任意两只蚂蚁碰头时,它们会同时调头朝相反方向走,速度不变。编写程序求所有蚂蚁都离开木杆的最短时间和最长时间。

解法:

我们假设每只蚂蚁都背着一袋粮食,任意两只蚂蚁碰头时交换各自的粮食然后调头。这种情况下,每次有一只蚂蚁离开木杆都意味着一袋粮食离开木杆(虽然可能已经不是它刚开始时背的那一袋了)。于是,我们可以求出每袋粮食离开木杆的时间(因为粮食是不会调头的)。又由于每袋粮食离开木杆的时间都对应某只蚂蚁离开木杆的时间,这是一种一一映射关系。所有蚂蚁都离开木杆的时间等于所有粮食都离开的时间,最长时间,就是所有粮食往离一端最远的方向走时取最大;最短时间,就是所有粮食往离一端最近的方向走时取最小。

扩展:

  1. 从左边数起的第i只蚂蚁什么时候走出木杆?
  2. 所有蚂蚁从一开始到全部离开木杆共碰撞了多少次?
  3. 第k次碰撞发生在哪个时刻?哪个位置?哪两个蚂蚁之间?
  4. 哪只蚂蚁的碰撞次数最多?
  5. 如果不是一根木杆而是一个铁圈,经过一段时间后所有蚂蚁都会回到的状态吗?这个时间的上界是多少?

扩展1. 

命题:若一开始时有M只蚂蚁向左走,N−M只蚂蚁向右走,则最终会有M只蚂蚁从木杆左边落下,N−M只蚂蚁从木杆右边落下。

这个命题很容易证明:每次碰撞均不改变向左和向右的蚂蚁数量。于是,由于每次碰撞蚂蚁都会调头而不是穿过,最后必定是从左边数起的前M只蚂蚁从左边落下,后N−M只蚂蚁从木杆右边落下。由于我们知道每袋粮食是从哪边落下的,故左边落下的M袋粮食的离开木杆的时间就对应于从左边数起的前M只蚂蚁离开木杆的时间,右边的类似。因此,我们只需判断i和M的关系,便知道第i只蚂蚁是从左边还是右边落下。不妨假设是从左边落下,因此该蚂蚁落下的时间就等于从左边落下的第i袋粮食的落下时间。时间复杂度O(N),扫一遍就知道所有粮食下落的时间,用一个数组来保存,前m个保存左边下落的粮食,后n-m个保存右边下落的粮食。如果i<=m,就可以找出第i个的时间。如果i>m,那么直接n-(i-m)就得到。

扩展2.

我们只需求得每个蚂蚁的碰撞次数,然后累加即可。在这里我们换一种思路,把碰撞调头看成不调头而继续向前(穿过),则容易看出原问题(碰撞次数)就变为求穿过的次数(因为速度大小不变,原来的每次碰撞都对应于现在的一次穿过)。则对于每只向左的蚂蚁,它只会“穿过”那些在它左边的向右走的蚂蚁。因此,每只蚂蚁“穿过”的蚂蚁次数等于刚开始时在它前进方向上与它前进方向相反的蚂蚁个数。时间复杂度也是O(N)。只要统计一下[0,i-1]在两个方向的计数,再与当前的蚂蚁的方向对比一下就可以在O(N)统计出来。

扩展3.

首先我们假设v为0.5个单位长度每秒,每个蚂蚁刚开始时都处于整点处,这样每次碰撞都发生在整秒处。这个假设有个好处,就是我们可以二分第k次碰撞的时刻。如果碰撞时刻是浮点数,这个二分有可能永远不会终止。我们还是看成每个蚂蚁驮着一袋粮食,那么每袋粮食易主(即从一个蚂蚁身上交换到另一个蚂蚁身上)时,就发生了一次碰撞。由于粮食的方向是固定不变的,我们可以很容易求出每一袋粮食在它的“前进”方向上的所有易主时刻(它易主的次数等于扩展2中的“穿过”次数)。这样,我们的问题就等价于:

找到最小的时间t,使得易主时刻小于或等于t的易主次数大于或等于k。

由于现在所有碰撞(易主)的时刻都是整点,故我们可以二分t,然后用线性复杂度找出易主时刻小于或等于t的易主次数。整个复杂度为O(N∗log(|maxt−mint|),其中maxt和mint分别表示第一次和最后一次碰撞的时刻,均可在O(N)时间内求出。

在上一段中,要想使用线性时间复杂度求出易主时刻小于或等于t的易主次数还需要一点技巧。可以这样:用一个数组pi表示第i个向右走的蚂蚁的初始位置,当扫描到第j个向左走的蚂蚁时,假设得到的中值点为i′(即p0到第pi′个位置上对应的粮食和该袋粮食的易主时刻均大于t)。由于该袋粮食向左易主的时刻是递增的,而下一个向左走的蚂蚁的初始位置又大于当前(第j个向左走的)蚂蚁,故对于下一个蚂蚁ant来说,p0到第pi′个位置上对应的粮食和ant所驮粮食的易主时刻也一定大于t。即中值点的位置是单调的。因此可以在均摊O(N)的时间内算出所求个数。

求出时刻的同时我们也求出了位置,故第二小问也解决。接下来要求哪两个蚂蚁发生了这次碰撞(如果同时存在多个碰撞求出任意一个即可)。其实,我们只需要求出每袋粮食在t时刻的位置即可。因为每袋粮食必然对应于一个驮着它的蚂蚁,故我们只需对这些粮食的位置排序,找出位置相同的粮食以及其下标(即从左到右第几个),也就找出了那对蚂蚁了。

扩展5.

这个问题看起来挺复杂,其实很简单。假设环长为L,则一个蚂蚁走完一圈需要T=L/v的时间。首先,还是像上面的讨论那样假设每个蚂蚁都驮着一袋粮食。那么,经过T时间后所有粮食都回到了原来的位置。由于每袋粮食都对应一个蚂蚁,而蚂蚁每次碰撞都会调头,因此蚂蚁的相对位置是不变的,这就说明经过T时间后蚂蚁循环移动了。假设移动了s个位置,即每个蚂蚁都到达它往右第s个蚂蚁的初始位置,那么,类似地,再经过T时间,当前状态仍会循环移动s个位置。容易看出这是一个最小公倍数问题:循环移动多少个s次之后每个蚂蚁回到自己位置?答案为LCM(N,s)/s,于是最多经过Tmax=LCM(N,s)/s∗T≤N∗T时间,每个蚂蚁都至少回到原地一次。

转自:http://lam8da.sinaapp.com/?p=11

原文地址:https://www.cnblogs.com/linyx/p/4002670.html