蚂蚁爬杆
http://lam8da.sinaapp.com/?p=11
《編程之美》4.7節描述了螞蟻爬桿問題,把所有具體數字都表示成字母后變為形如如下形式的問題:
有一根長為L的平行于x軸的細木桿,其左端點的x坐標為0(故右端點的x坐標為L)。剛開始時,上面有N只螞蟻,第i(1≤i≤N)只螞蟻的橫坐標為xi(假設xi已經按照遞增順序排列),方向為di(0表示向左,1表示向右),每個螞蟻都以速度v向前走,當任意兩只螞蟻碰頭時,它們會同時調頭朝相反方向走,速度不變。編寫程序求所有螞蟻都離開木桿需要多長時間。
該問題是經典問題了,有O(N)的解法。昨天和趙牛同學討論了該問題的一些擴展,趙牛均給出了精妙解答,現列出如下:
擴展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(|maxt?mint|),其中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)/s?T≤N?T時間,每個螞蟻都至少回到原地一次。
除了以上幾個擴展,還有一些個人認為比較變態的擴展,有的沒空仔細想,有的暫時沒想到解法,也列出如下,歡迎拍磚:
另外,趙牛同學又提出了一些更bt的擴展,如下:
總結
- 上一篇: 二分查找的各种条件
- 下一篇: 编程之美——4.11 扫雷游戏的概率