admin管理员组

文章数量:1573362

《编程之美》4.7节描述了蚂蚁爬杆问题,把所有具体数字都表示成字母后变为形如如下形式的问题:

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

该问题是经典问题了,有O(N)的解法。扩展问题现列出如下:

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

 

i只蚂蚁什么时候走出木杆?

原有的题目解释:

现在来解决扩展1。这个解答甚是精妙,通俗点来说,我们假设每只蚂蚁都背着一袋粮食,任意两只蚂蚁碰头时交换各自的粮食然后调头。这种情况下,每次有一只蚂蚁离开木杆都意味着一袋粮食离开木杆(虽然可能已经不是它刚开始时背的那一袋了)。于是,我们可以求出每袋粮食离开木杆的时间(因为粮食是不会调头的)。又由于每袋粮食离开木杆的时间都对应某只蚂蚁离开木杆的时间,这是一种一一映射关系。现在我们要找到对应于第i只蚂蚁的那个映射。在此之前需要证明一个命题:

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

这个命题很容易证明:每次碰撞均不改变向左和向右的蚂蚁数量。于是,由于每次碰撞蚂蚁都会调头而不是穿过,最后必定是前M只蚂蚁从左边落下,后NM只蚂蚁从木杆右边落下。由于我们知道每袋粮食是从哪边落下的,故左边落下的M袋粮食的离开木杆的时间就对应于前M只蚂蚁离开木杆的时间,右边的类似。因此,我们只需判断iM的关系,便知道第i只蚂蚁是从左边还是右边落下。不妨假设是从左边落下,因此该蚂蚁落下的时间就等于从左边落下的第i袋粮食的落下时间。时间复杂度O(N),一遍扫描搞定。

每每折腾智力类型的题目,不知道总在纠结些什么,明明知道些什么却不能解释(通俗说就是iq不够)。猜到故事的过程,却猜不到故事的结局。现在理解能力也直线下滑。花了很长时间看解释,怎么也看不懂!!!然后决定另起炉灶单干起来,百转千回之后,终于庆幸自己找到答案的时候;还来不及得瑟,发现自己理解了解释中的意思了。原来是这么的简单,而且解释也是这么明白,就是当时理解不过来;而且它的比自己的做的要好多了。

关键点就在标红得字里,如果你理解了,估计也不要往下看了。

 红色的字说明,准确描述应该是,左边起前M只蚂蚁必然是从左边落下的。而剩余的从右边起得N-M只蚂蚁是从右边下了的。为什么呢?

试想,每一只蚂蚁的相对位置是不会改变的,即:

如果蚂蚁a在蚂蚁b的左边:

  • 如果不掉下去,不管经过了多少次碰头,和谁碰头,蚂蚁a必然还会在蚂蚁b的左边;
  • 如果都往左掉下去了,蚂蚁a也是先于蚂蚁b掉下去,我们也定义为a在b左边;
  • 如果都往右掉下去了,蚂蚁b则先于蚂蚁a掉下去,我们还是定义a左于b而后掉下去;
  • 如果不同方向掉下去,必然是a从左边掉下去,b从右边掉下去,依然定义a左与b掉下去。

而解释中证明了如果开始M个向左,N-M个向右,最终会有M个从左边掉下去,而N-M个从右边掉下去;

这样初始状态位置在前M个(左边数起)的蚂蚁必然要从左边掉下去(不管初始方向),因为他们之间的相对位置是不会改变的。证明:假设第i(i<m)个从右边出去了,根据他们的相对位置不会改变,则有从i其所有的右边的蚂蚁都要从右边掉下,这样就有超过了N-M个蚂蚁从右边掉下去了(注意i<m),与前一个证明的结论矛盾,证毕。

文字太抽象,上图表结论:

→ → ← → ← → → ← ← → → ← ← → → ← →  初始10个向右,7个向左;它们对应出去的方向为:

← ← ← ← ← ← ← → → → → → → → → → →

所以原解释中也说得相当的明白,就是当时未能理解。所以就可以根据i和M的关系判断出,蚂蚁出去的方向了。这样就可以简单的一次扫描出结果了。

又是挫计,给出自己当时所想的代码,可能对理解有帮助:

  1 #include <iostream>
  2 #include <bitset>
  3 #include <vector>
  4 #include <deque>
  5 
  6 using namespace std;
  7 
  8 typedef struct _ateinfo_t
  9 {    
 10     int x;
 11     int d;
 12     int outnum;  //出去的编号
 13     int outd;    //出去的方向
 14 }ateinfo_t; 
 15 
 16 #define PRINT(ainfo, it, mem) for ((it)=(ainfo).begin(); (it)!=(ainfo).end(); ++(it)) \
 17 {     \
 18     cout<< (it)->##mem##<<" ";    \
 19 }    \
 20 cout<<endl
 21 
 22 
 23 int func28(vector<ateinfo_t> & ainfo, int len,int i)
 24 {
 25     vector<ateinfo_t>::iterator it;
 26     deque<int> lout_time;  //从左边出去的 时间排序
 27     deque<int> rout_time;    //从右边出去的 时间排序
 28     int lc=0; //lc 为当前节点(不包括当前节点)的左边 向右方向走的个数
 29     int rc=0; //rc 为当前节点(不包括当前节点)的右边 向左方向走的个数
 30     //根据上述的两个值就可以确定当前节点是从那个方向出去的
 31     int lout=1; //记录左边出去的编号
 32     int rout;    //记录右边出去的编号
 33 
 34 
 35     for (it=ainfo.begin(); it!=ainfo.end(); ++it)
 36     {
 37         if (it->d ==1)
 38         {
 39             lc++;
 40         }
 41     }
 42     rout = ainfo.size() - lc; //
 43 
 44     for (it=ainfo.begin(); it!=ainfo.end(); ++it)
 45     {
 46         
 47         if (it->d == 0) //right
 48         {
 49             int t = (len - it->x)/1; //v=1
 50             rout_time.push_front(t); //
 51             if (rc - lc>= 0)
 52             {
 53                 //right
 54                 it->outd = 0;
 55                 it->outnum = rout--;
 56             }
 57             else
 58             {
 59                 //left
 60                 it->outd = 1;
 61                 it->outnum = lout++;
 62             }
 63             ++rc;
 64         }
 65         else
 66         {
 67             int t = (it->x)/1; //v=1
 68             lout_time.push_back(t); //
 69             
 70             lc--; //减其本身
 71 
 72 
 73             if (rc - lc>= 1)  //注意为0情况
 74             {
 75                 //right
 76                 it->outd = 0;
 77                 it->outnum = rout--;
 78             }
 79             else
 80             {
 81                 //left
 82                 it->outd = 1;
 83                 it->outnum = lout++;
 84             }            
 85 
 86         }
 87                 
 88     }
 89 
 90     PRINT(ainfo, it, x);
 91     PRINT(ainfo, it, d);
 92     PRINT(ainfo, it, outd);
 93     PRINT(ainfo, it, outnum);
 94 
 95     deque<int>::iterator iit;
 96     cout<<"Left out time:";
 97     for (iit = lout_time.begin(); iit!=lout_time.end(); ++iit)
 98     {
 99         cout<<*iit<<" ";
100     }
101     cout<<endl;
102 
103     cout<<"Right out time:";
104     for (iit = rout_time.begin(); iit!=rout_time.end(); ++iit)
105     {
106         cout<<*iit<<" ";
107     }
108     cout<<endl;
109 
110     
111     if(ainfo.at(i).outd ==0) //right out
112     {
113         return rout_time.at(ainfo.at(i).outnum-1);
114     }
115     else
116     {
117         return lout_time.at(ainfo.at(i).outnum-1);
118     }
119 }
120 
121 int main()
122 {
123         vector<ateinfo_t> ainfo;
124     int b[] = {1,4,8,10,13,16};
125     int o[] = {0,0,1,0,1,1};
126     int len =19;
127 
128     for (i=0; i<sizeof(b)/sizeof(int);++i)
129     {
130         ateinfo_t tm;
131         tm.x = b[i];
132         tm.d = o[i];
133         ainfo.push_back(tm);
134     }
135 
136     cout<<func28(ainfo, len, 3)<<endl;
137 
138 
139 
140     
141     return 0;
142 }

1 4 8 10 13 16
0 0 1 0 1 1
1 1 1 0 0 0
1 2 3 3 2 1
Left out time:8 13 16
Right out time:9 15 18
18

 

 P.S. 算法虐我千百遍,我当算法如初恋。。。。

其他扩展解释附上:

扩展2的解答

对于扩展2,我们只需求得每个蚂蚁的碰撞次数,然后累加即可。在这里我们换一种思路,把碰撞调头看成不调头而继续向前(穿过),则容易看出原问题(碰撞次数)就变为求穿过的次数(因为速度大小不变,原来的每次碰撞都对应于现在的一次穿过)。则对于每只向左的蚂蚁,它只会“穿过”那些在它左边的向右走的蚂蚁。因此,每只蚂蚁“穿过”的蚂蚁次数等于刚开始时在它前进方向上与它前进方向相反的蚂蚁个数。时间复杂度也是O(N)。改为用粮食的观点来理解也是可以的。

扩展3的解答

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

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

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

在上一段中,要想使用线性时间复杂度求出易主时刻小于或等于t的易主次数还需要一点技巧。可以这样:用一个数组pi表示第i个向右走的蚂蚁的初始位置,当扫描到第j个向左走的蚂蚁时,假设得到的中值点为i(即p0到第pi个位置上对应的粮食和该袋粮食的易主时刻均大于t)。由于该袋粮食向左易主的时刻是递增的,而下一个向左走的蚂蚁的初始位置又大于当前(第j个向左走的)蚂蚁,故对于下一个蚂蚁ant来说,p0到第pi个位置上对应的粮食和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,于是最多经过Tmax=LCM(N,s)/sTNT时间,每个蚂蚁都至少回到原地一次。

除了以上几个扩展,还有一些个人认为比较变态的扩展,有的没空仔细想,有的暂时没想到解法,也列出如下,欢迎拍砖:

  1. 如果每只蚂蚁的速度不一样(这就有可能由于追赶而产生碰撞,此时根据动量守恒定律:(,速度互换),上述扩展问题的答案是什么呢?
  2. 如果蚂蚁在一个平面上运动,同样也是碰头后原路返回(注意这不等同于两只蚂蚁交换继续前进),问是否所有蚂蚁都能最终离开平面?
  3. 在上述情况下,如果最终能离开平面,离开平面需要多长时间?
  4. 在上述情况下,回答关于一维的前文讨论的每个问题。

另外,赵牛同学又提出了一些更bt的扩展,如下:

  1. 假设每个蚂蚁都有重量,两只蚂蚁碰撞时轻的那个有一定几率从旁边被撞下去:(,那又该怎样?
  2. 假设不是被撞下去而是有一定几率被撞晕而停滞几秒,那又该怎样?
  3. blablabla...

转载于:https://wwwblogs/legendmaner/archive/2013/04/18/3027269.html

本文标签: 蚂蚁