本文始發於我的公衆號:TechFlow算法
最近看到一道頗有意思的問題,分享給你們。框架
仍是老規矩,在咱們聊算法問題以前,先來看一個故事。函數
傳說中,有5個海盜組成了一支無敵的海盜艦隊,他們在最後一次的尋寶當中找尋到了100枚價值連城的金幣。因而,很天然的,這羣海盜面臨分贓的問題。爲了防止海盜內訌,殘忍的海盜們制定了一個奇怪的規則:測試
他們決定按照功勞大小對五我的進行編號,由編號小的海盜先提出分配方案。若是方案可以獲得大多數人的贊成,那麼就按照他提出的方案進行分配。若是不能經過,說明他已經失去了威望,海盜們會殘忍地將他投入海中喂鯊魚。code
在一個朦朧的早上你一覺醒來,忽然發現本身成了一號海盜,那麼你應該如何分配才能得到最多的金幣,又不會被喂鯊魚呢?orm
在咱們思考以前,咱們先完善一下題意,增長几個條件。blog
首先,每個海盜都很是殘忍。這意味着,在不影響收益的狀況下,他們會更傾向於殺人。遞歸
其次,每個海盜都極其聰明,都能想到最佳答案。io
這兩個條件一出來,問題就比較明顯了,這是博弈論題目纔有的架勢。form
既然這是一道博弈論的問題,那麼咱們經過常規的思路是沒法找到答案的,咱們須要另闢蹊徑才行。
那麼,怎麼另闢蹊徑呢?
一個比較常規的作法是先不考慮原問題,先假設一個和原問題差很少,可是規模小不少的子問題。經過對子問題的求解來摸索原問題的解法。
舉個例子,在這題當中,咱們須要計算5個海盜分金幣的狀況。一時之間咱們有些無從下手,那麼咱們簡化問題,問題的規則仍是不變,可是咱們把海盜的數量減小,減小到只有一個海盜。那麼根據規則,很顯然,最後的結果是這個海盜獨吞全部的金幣。
這個時候的分配方案是:[0, 0, 0, 0, 100]
咱們從這個點開始往回倒推,假設這個時候多了一個海盜,一共是4號和5號兩個海盜的時候,會怎麼樣?
顯然由於要求要一半以上贊成提案,提案才能夠經過。因此在這個時候,不管4號海盜如何提議,5號都不會贊成,要將他投下海喂鯊魚。因此若是隻剩下4和5的時候,4號海盜必死無疑。
這個時候的分配方案是: [0, 0, 0, -1, 100],-1表示必死無疑。
那若是再加一個海盜呢?
再加一個海盜的話,是3,4,5三個海盜的狀況。由於只剩4和5的時候4號必死,因此他爲了活命必定會贊成3號的提案(海盜對其餘人殘忍,對本身不殘忍)。這個時候,3號不論如何提議,都必定能夠經過。由於算上他本身的一票,和4號的一票,已通過半了,因此他的提案必定能夠經過。
這個時候的分配方案是: [0, 0, 100, 0, 0]
咱們再加入一個海盜,考慮一共剩下4個海盜的狀況。若是2號死去,那麼3號能夠獨吞全部金幣,因此顯然3號必定不會贊成2號的方案。4我的的時候,至少須要3我的贊成才能夠經過方案,那麼2號必需要爭取4號和5號。若是2號死去,4號和5號一無全部,因此2號只須要分配給4號和5號一枚金幣,就能夠拉攏他們。
這個時候的分配方案是: [0, 98, 0, 1, 1]
最後,咱們再加入1號海盜。同理,1號海盜的提案須要至少3我的經過。算上他本身,他還須要爭取2票。因爲1號死去2號能夠得到98枚金幣,因此1號必定沒法爭取2號,仍是隻能從3,4,5三我的下手。能夠給3號1枚,4號兩枚(比2號的方案多一枚),也能夠給3號1沒,5號兩枚。
這個時候的分配方案是: [97, 0, 1, 2, 0] 或者是 [97, 0, 1, 0, 2]。
到這裏,這個問題就結束了。可是咱們的思考並無結束,不知道你們從剛纔的解法當中有沒有看出規律。咱們面臨5個海盜這種錯綜複雜狀況的時候根本無從下手,可是一旦當咱們試着將問題的規模縮小,從簡單的狀況開始思考,那麼問題一會兒就豁然開朗了。
老子說:天下大事,必做於細,天下難事,必做於易。從這個問題來看,和這個道理相得益彰。
這種從最簡單推導最複雜的算法就稱爲遞歸。
假設,獲取n個海盜分配方案的函數是f。當咱們計算f(2)時,咱們須要根據f(1)的結果。咱們試着寫成僞代碼:
def f(n): if n == 1: return [0, 0, 0, 0, 100] else: allocation = f(n-1) # 新的分配 new_allocation = allocate(allocation) return new_allocation
咱們先忽略allocate這個方法內部是怎麼實現的,單純看這段代碼,整個框架已經有了。
遞歸的精髓也就在這裏,程序本身調用本身只是表象,內裏的精髓實際上是問題的分割。整個遞歸從上到下的過程,其實是一個大問題化解成小問題的過程。若是還不明白,咱們再來看一個經典的例子來鞏固一下,這個問題就是大名鼎鼎的漢諾塔問題:
在印度神話當中有一個大神叫作梵天,他在創造世界的時候創造了三根金剛柱。爲了排解無聊,他在其中一根柱子上擺放了64個圓盤。這64個圓盤從上往下依次增大,他給僧侶出了一個問題。一次只能移動一個圓盤,而且圓盤只能放在比它大的圓盤上,該怎麼作才能將圓盤從一根柱子移動到另外一根呢?
爲了簡化問題,咱們先觀察擺放5個圓盤的狀況。從圖中能夠看出來,一開始的時候圓盤都在A柱,若是咱們想要將圓盤移動到B柱應該怎麼辦呢?
咱們一樣先來觀察最簡單的狀況: A柱上只有一個圓盤,那很簡單,咱們直接將它移動到B柱便可。若是有兩個圓盤呢?咱們須要先將第一個移動到C柱,而後將第二個移動到B柱,最後再將C柱上的圓盤移動到B。那若是是三個圓盤呢,稍微複雜一些,但仔細列舉一下,也能算得出來。
可是咱們怎麼經過問題規模的縮小來化簡問題呢?
這須要咱們對於題目進行深刻思考,找到其中的關鍵點。這題的關鍵點就是圓盤的限制,大的圓盤不能落在小的圓盤上面。因此若是咱們想要將n個圓盤從A柱移動到B柱,必需要將前n-1個圓盤先移動到C柱,這樣才能夠將最大的那塊放到B,如此以後再將n-1塊移動回B。
也就是說,咱們將n-1塊圓盤當作是一個總體,這樣n塊圓盤的方案就和兩塊圓盤時同樣了。這就經過遞歸完成了簡化。
最後,也是最關鍵的,怎麼移動n-1塊圓盤呢?其實很簡單,咱們套用一樣的方法,再將這n-1塊圓盤中的n-2塊當作是總體,遞歸操做。理解了以後,不妨試着寫出代碼,其實只有幾行:
def hanoi_tower(num, tower_start, tower_dest, tower_other): if num == 1: print('move plate {} from {} to {}'.format(num, tower_start, tower_dest)) return hanoi_tower(num-1, tower_start, tower_other, tower_dest) print('move plate {} from {} to {}'.format(num, tower_start, tower_dest)) hanoi_tower(num-1, tower_other, tower_dest, tower_start)
咱們調用一下這個方法,進行一下測試:
結果和咱們的預期一致,說明咱們的算法是正確的。
最後,咱們再回到海盜問題,又該怎麼用代碼實現呢?感興趣的同窗不妨親自動手試試,若是實在寫不出代碼,在公衆號回覆關鍵詞」海盜分金「查看我寫的代碼。
今天的文章就到這裏,掃碼關注個人公衆號:TechFlow,獲取更多文章