程式設計之美:螞蟻爬杆問題的擴充套件

fangjian1204發表於2014-08-19

《程式設計之美》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. 如果不是一根木杆而是一個鐵圈,經過一段時間後所有螞蟻都會回到的狀態嗎?這個時間的上界是多少?

擴充套件1的解答

現在來解決擴充套件1。這個解答甚是精妙,通俗點來說,我們假設每隻螞蟻都揹著一袋糧食,任意兩隻螞蟻碰頭時交換各自的糧食然後調頭。這種情況下,每次有一隻螞蟻離開木杆都意味著一袋糧食離開木杆(雖然可能已經不是它剛開始時背的那一袋了)。於是,我們可以求出每袋糧食離開木杆的時間(因為糧食是不會調頭的)。又由於每袋糧食離開木杆的時間都對應某隻螞蟻離開木杆的時間,這是一種一一對映關係。現在我們要找到對應於第i只螞蟻的那個對映。在此之前需要證明一個命題:

若一開始時有M只螞蟻向左走,NM只螞蟻向右走,則最終會有M只螞蟻從木杆左邊落下,NM只螞蟻從木杆右邊落下。

這個命題很容易證明:每次碰撞均不改變向左和向右的螞蟻數量。於是,由於每次碰撞螞蟻都會調頭而不是穿過,最後必定是從左邊數起的前M只螞蟻從左邊落下,後NM只螞蟻從木杆右邊落下。由於我們知道每袋糧食是從哪邊落下的,故左邊落下的M袋糧食的離開木杆的時間就對應於從左邊數起的前M只螞蟻離開木杆的時間,右邊的類似。因此,我們只需判斷iM的關係,便知道第i只螞蟻是從左邊還是右邊落下。不妨假設是從左邊落下,因此該螞蟻落下的時間就等於從左邊落下的第i袋糧食的落下時間。時間複雜度O(N),一遍掃描搞定。

擴充套件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...

相關文章