1. 程式人生 > >演算法淺談——遞迴演算法與海盜分金問題

演算法淺談——遞迴演算法與海盜分金問題

本文始發於個人公眾號:TechFlow


最近看到一道很有意思的問題,分享給大家。

還是老規矩,在我們聊演算法問題之前,先來看一個故事。


傳說中,有5個海盜組成了一支無敵的海盜艦隊,他們在最後一次的尋寶當中找尋到了100枚價值連城的金幣。於是,很自然的,這群海盜面臨分贓的問題。為了防止海盜內訌,殘忍的海盜們制定了一個奇怪的規則:


他們決定按照功勞大小對五個人進行編號,由編號小的海盜先提出分配方案。如果方案能夠得到大多數人的同意,那麼就按照他提出的方案進行分配。如果不能通過,說明他已經失去了威望,海盜們會殘忍地將他投入海中喂鯊魚。

在一個朦朧的早上你一覺醒來,突然發現自己成了一號海盜,那麼你應該如何分配才能獲得最多的金幣,又不會被喂鯊魚呢?


在我們思考之前,我們先完善一下題意,增加幾個條件。


首先,每一個海盜都非常殘忍。這意味著,在不影響收益的情況下,他們會更傾向於殺人。

其次,每一個海盜都極其聰明,都能想到最佳答案。


這兩個條件一出來,問題就比較明顯了,這是博弈論題目才有的架勢。

既然這是一道博弈論的問題,那麼我們通過常規的思路是無法找到答案的,我們需要另闢蹊徑才行。


那麼,怎麼另闢蹊徑呢?


一個比較常規的做法是先不考慮原問題,先假設一個和原問題差不多,但是規模小很多的子問題。通過對子問題的求解來摸索原問題的解法。


舉個例子,在這題當中,我們需要計算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,獲取更多文章

相關推薦

演算法——演算法海盜問題

本文始發於個人公眾號:TechFlow 最近看到一道很有意思的問題,分享給大家。 還是老規矩,在我們聊演算法問題之前,先來看一個故事。 傳說中,有5個海盜組成了一支無敵的海盜艦隊,他們在最後一次的尋寶當中找尋到了100枚價值連城的金幣。於是,很自然的,這群海盜面臨分贓的問題。為了防止海盜內訌,殘忍的海盜們

演算法

1 引言   程式呼叫自身的程式設計技巧稱為遞迴( recursion)。遞迴作為一種演算法在程式設計語言中廣泛應用。一個方法或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞迴策略只需要少量的程式就可以描述出解題過程所需要的多

演算法——走迷宮問題廣度優先搜尋

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注 在之前週末LeetCode專欄當中,我們詳細描述了深度優先搜尋和回溯法,所以今天我們繼續這個話題,來和大家聊聊搜尋演算法的另一個分支,廣度優先搜尋。 廣度優先搜尋的英文是Breadth First Search,簡寫為bfs。與它相對的深

資料結構實驗-C語言-二叉樹的建立,前、中、後序遍歷的遞迴演算法和非遞迴演算法,求葉子結點數目,求二叉樹深度,判斷二叉樹是否相似,求二叉樹左右子樹互換,二叉樹層序遍歷的演算法,判斷二叉樹是否是完全二叉樹

1.實驗目的 熟練掌握二叉樹的二叉連結串列儲存結構的C語言實現。掌握二叉樹的基本操作-前序、中序、後序遍歷二叉樹的三種方法。瞭解非遞迴遍歷過程中“棧”的作用和狀態,而且能靈活運用遍歷演算法實現二叉樹的其它操作。 2.實驗內容 (1)二叉樹的二叉連結串列的建立 (2)二叉樹的前、中、後

Java的理解

      遞迴方法其實就是一個直接或者間接呼叫自己的方法,是一個簡單、實用的方法,在遇到某些不好解決的問題時,可以用遞迴方法來解決,比如:求一段連續自然數之間的和、階乘等等。下面我簡單講講遞迴方法到底是怎麼個情況。       先來一段程式碼: public clas

(一)】的基本思想

1、遞迴簡述 遞迴作為程式設計裡最為重要的程式設計方法之一,其對於解決某些複雜的問題十分有效,並且相對於迭代,其過程在直觀上更容易理解。而且不像迭代自己需要維護許多變數,遞迴也更容易實現。 2、遞迴的基本思想 遞歸併不是簡單的自己呼叫自己,也不是簡單的互動

斐波那契數列演算法和非演算法以及其時間複雜度分析

1、在學習資料結構這門課的過程中,發現斐波那契數列的遞迴演算法以及非遞迴演算法,以及其時間複雜度分析是一個小難點。所以特別總結一下。 斐波那契數列的表示式: Fibonacci數列簡介: F(1)=

經典演算法之非演算法實現二叉樹前、中、後序遍歷

/************************ author's email:[email protected] date:2017.12.24 非遞迴演算法實現二叉樹前、中、後序遍歷 ************************/ #include<

全排列演算法之Perm演算法實現

題目描述: 給定一個由不同的小寫字母組成的字串,輸出這個字串的所有全排列。 我們假設對於小寫字母有'a' < 'b' < ... < 'y' < 'z',而且給定的字串中的字母已經按照從小到大的順序排列。 輸入: 輸入只有一行,是一個由不同的小寫字母組成的字串,已知字串的長度在1

三種快速排序演算法的實現(演算法、非演算法、三路劃分快速排序)

快速排序的三個步驟: 1、分解:將陣列A[l...r]劃分成兩個(可能空)子陣列A[l...p-1]和A[p+1...r],使得A[l...p-1]中的每個元素都小於等於A(p),而且,小於等於A[p

求最大公約數和最小公倍數(演算法及非演算法

最近做題目發現一些題目需要求數的最大公約數和最小公倍數,想想最大公約數和最小公倍數平時做數學的時候感覺不是很難,但是突然要程式設計來實現,卻一下子不知所措了,後來看了下別人寫的,發現其實也不算特別難。最小公倍數其實只要一個公式,即整數A和整數B的最小公倍數為A*B/gcd(

在做leetcode的時候,遇到了二叉樹的遍歷,用過許多方法之後,最終著手於利用遞迴方法來遍歷二叉樹,遞迴演算法在實現上相較於其他的演算法來得更為簡單,但是遞迴對時間和空間的佔用其實是比較高的,所以當選擇遞迴的時候,我們應該考慮是否有更加便捷的非遞迴方法。那麼什麼情況下我們可

Python漢諾塔問題演算法程式

漢諾塔問題: 問題來源:漢諾塔來源於印度傳說的一個故事,上帝創造世界時作了三根金剛石柱子,在一根柱子上從上往下從小到大順序摞著64片黃金圓盤。上帝命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一回只能移動一個圓盤,只能移動在最頂端的圓盤。有預言說

[計算機程式設計C++] Fibonaci數列的演算法實現

本文是對西安交通大學C++慕課第三章程式設計練習的16題的講解。 參考部落格:https://blog.csdn.net/zombie_slicer/article/details/38871799 題目內容: 編寫程式,顯示Fibonaci序列的前n項(從

Fibonacci數列演算法演算法

轉載於:http://blog.csdn.net/qq_33951180/article/details/52484080 一、斐波那契數列  由於斐波納挈數列是以兔子的繁殖引入的,因此也叫“兔子數列”。它指的是這樣一個數列:0,1,1,2,3,5,8,13……從這組數可以很明顯看出這

貪心演算法演算法,動態規劃演算法比較總結

一般實際生活中我們遇到的演算法分為四類: 一>判定性問題 二>最優化問題 三>構造性問題 四>計算性問題 而今天所要總結的演算法就是著重解決 最優化問題 《演算法之道》對三種演算法進行了歸納總結,如下表所示: 分

揹包問題的演算法

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

二叉樹先序遍歷、中序遍歷、後序遍歷的演算法演算法

首先是二叉樹資料結構的定義: typedef struct TNode *Position; typedef Position BinTree; /* 二叉樹型別 */ struct TNode{ /* 樹結點定義 */ int Data; /* 結點資料 */ BinTre

函式二分查詢演算法

一、遞迴函式 1.遞迴呼叫的定義 遞迴呼叫是函式巢狀呼叫的一種特殊形式,函式在呼叫時,直接或間接呼叫了自身,就是遞迴呼叫 def foo(n): print(n) n += 1 foo(n) foo(1) 2.遞迴最大深度 最大遞迴深度預設是

【資料結構演算法】之的基本介紹---第六篇

一、遞迴的基本概念 1、定義 遞迴:指的是一個過程,函式直接或者間接的呼叫自己,此時則發生了遞迴。 遞迴的兩個要素:遞推公式和遞迴邊界 可以看到遞迴的定義非常的簡潔,但是理解起來就沒有這麼容易了。不知道大家是否和我一樣,在遇到遞迴問題的時候,總是試圖去一步一步的分