如何理解漢諾塔的遞歸?

學C++ 遞歸篇 看懂了斐波那契序列的遞歸 但是卻死也看不懂移動漢諾塔

主要問題有兩個

  1. 怎麼把一個大的移動問題分解成三部分?而這三部分恰好是要遞歸的三步
  2. 遞歸的三步如何完美的控制(明明三步中嵌套兩步 思維亂死了)


2017.11.13 校對說明:

才發現題主想問的是編程……這是筆者以前寫的一篇文章,純粹解釋漢諾塔的數學問題,沒有直接給出編程方案,獲得最高贊數也是很醉……總之能夠幫到大家理解就好,具體的編程可以參考其他答主的答案。

-

懶漢式遞歸——瞬間明白漢諾塔問題

Q. 為什麼會有遞歸?

A. 因為我們是人,不是電腦!我們的working

memory有限!

遊戲規則:

有A,B,C三根針,將A針上N個從小到大疊放的盤子移動到C針,一次只能移動一個,不重複移動,小盤子必須在大盤子上面。

問題:

總的移動次數是多少?

分析:

首先明確,我們的目標是將A針上所有N個盤子移動至C針。而對於B針,我們可以將之看成一個中轉站。

這個問題,順向思維或者逆向思維道理是相同的,都太麻煩。我們不妨從中間開始思考。

||: 規則要求小盤子必須在大盤子之上。試想這個過程中,必然會經歷那麼一個步驟,即有一大坨N-1個盤子在B針這個中轉站,而我們正將最大那個盤子(即第N個盤子)從A針移動至C針。

【圖例】

只有經歷「移動最大盤子」這個步驟,餘下的事情才有可能實現。而在此之前,我們所要做的事情,就是讓「移動最大盤子」這個步驟得以實現。

現在,遊戲整個過程以「移動最大盤子」為中央,被分為了兩部分。即(前)「將那坨N-1個盤子從A針移動到B針」,(中)「移動最大盤子」,(後)「將坨N-1個盤子從B針移動到C針」。

這是我們意識到,(前)與(後)操作道理是相似的。不去管那個最大盤子,(前)是以C針為中轉站,(後)是以A針為中轉站。因此兩者所需的移動次數應當是相等的。這意味著我們只要計算出其中一者的移動次數,然而乘以2,在加上「移動最大盤子」的那1次,就是這場遊戲的總移動次數了。

用數學語言表達,假設(前)「將N-1個盤子從A針移動到B針」所需次數為Hn-1,總移動次數為Hn,那麼可以得出的關係就是:Hn=Hn-1

x 2 + 1.

其實當我們得出這個算式的時候,稍微聰明一點的人已經明白,這就是一個遞推公式,可以直接用此公式得出Hn的通解。

但是LZ比較笨,就是不明白,為什麼這個公式就可以套用呢?

那麼就乾脆繼續思考吧。

讓我們再想像一個情景:最大那個盤子在剛剛從A針被移動到C針,而那坨N-1個盤子還在B針蠢蠢欲動地等待著,即處於(中)-&>(後)的這個狀態。

怎麼移動這N-1個盤子呢?

其實這時候,問題已經回到了筆者標示「||:」符號的地方。「||:」是樂譜中的反覆記號,而我們要做的,就是重複上面的步驟,但是要將N替換為N-1,因為現在只剩下N-1個盤子需要移動。而中轉站則從B變成了A(鑒於這時盤子都在B針)。目標仍然是C針。下一次重複的時候,只剩下N-2個盤子需要移動,中轉站又回到B,目標不變仍然是C針。……整個過程中,變化的只是中轉站(在A與B之間輪換),以及剩下那些所需要移動的盤子的總數(越來越少)而已。

那麼那個大盤子怎麼辦?不去管它嗎??

正解!!

因為你已經把它移到C針,已經完成了這個移動步驟,它不會影響之後的操作。提醒自己牢記遊戲規則,大盤子永遠在小盤子下面,而你也不需要再重複移動它——「不重複移動」,正是遊戲規則的要求!

於是

Hn=Hn-1 x 2 + 1 這個公式,就可以套用、套用、套用……直到H3=7,H2=3,H1=1。

最後,用最懶的數學歸納法證明通項公式

Hn = 2^n - 1 吧!沒辦法,LZ就是比較懶嘛~

Fin.


漢諾塔的話Concrete Mathematics第一章就講了,我覺得講得挺好。漢諾塔的話代碼不難寫,正確性也不難證明,稍微麻煩一點的是證明這是最小步數。第一章還講了recurrance。記得把題也做了。不理解遞歸可以好好刷刷數學歸納法。

當然寫到代碼用到遞歸就得刷調用棧,棧幀,了解下calling convention。這個隨便研究下就行了,弄gdb單步調試調試,打出backtrace看看。

數學歸納法和調用棧都了解之後遞歸就不是玄學了。

別人已經講了 F(n) = 2 F(n-1) + 1,F(1) = 1 的各種來歷和列印方式,我也不贅述了。

----------------- 為什麼 F(n) = 2 F(n-1) + 1,F(1) = 1 是最優的 -----------------

其實用數學歸納法就行了。

最基本的情況,即n為1,就不用說了。

現在F(n-1)確實是把n-1個盤子集體挪動的最小步數,我們要證明F(n)是把n個盤子集體挪動的最小步數。

1)在把n個盤子從A移動到C的過程中,必然存在一步,是把最大的盤子從A拿出來。要想把最大的盤子從A移動到別的某個柱子上(B或C),就必須保證剩下的n-1個盤子不能礙事,得好好堆在剩下那個柱子(C或B)上。要保證n-1個盤子都在剩下那個柱子上,至少得付出F(n-1)次移動。

2)在把n個盤子從A移動到C的過程中,必然存在一步,是最大的盤子被放到了C上,而且此後再也沒動過。在這步實行之前,最大的盤子要麼在A要麼在B上,而相應地別的n-1個盤子要麼在B要麼在A上。在這步實施之後,我們只要花至少F(n-1)的步數把n-1個盤子從要麼B要麼A挪動到C上就行了。這些步數必然和1)中的步數不重疊,因為這時候最大盤子在C上,而1)中最大盤子在A上。

3)最大的盤子至少被挪動了一次。而且這一次肯定沒被算在1)或2)的「至少F(n-1)步」中,因為後者只挪動較小的那n-1個盤子。

把1),2),3)加起來,就是至少F(n-1) + F(n-1) + 1步。不能再少了。


對遞歸的理解的要點主要在於放棄!

放棄你對於理解和跟蹤遞歸全程的企圖,只理解遞歸兩層之間的交接,以及遞歸終結的條件。

想像你來到某個熱帶叢林,意外發現了十層之高的漢諾塔。正當你苦苦思索如何搬動它時,林中出來一個土著,毛遂自薦要幫你搬塔。他名叫二傻,戴著一個草帽,草帽上有一個2字,號稱會把一到二號盤搬到任意柱。

你靈機一動,問道:「你該不會有個兄弟叫三傻吧?」

「對對,老爺你咋知道的?他會搬一到三號盤。「

」那你去把他叫來,我不需要你了。「

於是三傻來了,他也帶著個草帽,上面有個3字。

你說:」三傻,你幫我把頭三個盤子移到c柱吧。「

三傻沉吟了一會,走進樹林,你聽見他大叫:」二傻,出來幫我把頭兩個盤子搬到C!「

由於天氣炎熱你開始打瞌睡。朦朧中你沒看見二傻是怎麼工作的,二傻幹完以後,走入林中大叫一聲:「老三,我幹完了!」

三傻出來,把三號盤從A搬到B,然後又去叫二傻:「老二,幫我把頭兩個盤子搬回A!」

餘下的我就不多說了,總之三傻其實只搬三號盤,其他叫二傻出來干。最後一步是三傻把三號盤搬到C,然後呼叫二傻來把頭兩個盤子搬回C

事情完了之後你把三傻叫來,對他說:「其實你不知道怎麼具體一步一步把三個盤子搬到C,是吧?」

三傻不解地說:「我不是把任務幹完了?」

你說:「可你其實叫你兄弟二傻幹了大部分工作呀?」

三傻說:「我外包給他和你屁相干?」

你問到:「二傻是不是也外包給了誰?「

三傻笑了:「這跟我有屁相干?」

你苦苦思索了一夜,第二天,你走入林中大叫:「十傻,你在哪?」

一個頭上帶著10號草帽的人,十傻,應聲而出:「老爺,你有什麼事?」

「我要你幫把1到10號盤子搬到C柱「

「好的,老爺。「十傻轉身就向林內走。

「慢著,你該不是回去叫你兄弟九傻吧「

「老爺你怎麼知道的?「

「所以你使喚他把頭九個盤子搬過來搬過去,你只要搬幾次十號盤就好了,對嗎?「

「對呀!「

「你知不知道他是怎麼乾的?「

「這和我有屁相干?「

你嘆了一口氣,決定放棄。十傻開始幹活。樹林里充滿了此起彼伏的叫聲:「九傻,來一下!「 「老八,到你了!「「五傻!。。。「」三傻!。。。「」大傻!「

你注意到大傻從不叫人,但是大傻的工作也最簡單,他只是把一號盤搬來搬去。

若干年後,工作結束了。十傻來到你面前。你問十傻:「是誰教給你們這麼幹活的?「

十傻說:「我爸爸。他給我留了這張紙條。」

他從口袋裡掏出一張小紙條,上面寫著:「照你帽子的號碼搬盤子到目標柱。如果有盤子壓住你,叫你上面一位哥哥把他搬走。如果有盤子佔住你要去的柱子,叫你哥哥把它搬到不礙事的地方。等你的盤子搬到了目標,叫你哥哥把該壓在你上面的盤子搬回到你上頭。「

你不解地問:「那大傻沒有哥哥怎麼辦?「

十傻笑了:「他只管一號盤,所以永遠不會碰到那兩個『如果』,也沒有盤子該壓在一號上啊。」

但這時他忽然變了顏色,好像泄漏了巨大的機密。他驚慌地看了你一眼,飛快地逃入樹林。

第二天,你到樹林里去搜尋這十兄弟。他們已經不知去向。你找到了一個小屋,只容一個人居住,但是屋裡有十頂草帽,寫著一到十號的號碼。


我覺得這樣理解最高分的答案會比較簡單(粗暴)~

一個環:

Step1.將最大的環從A移動到C

A -&> C

兩個環:

Step1.把除了最大的環之外的環,從A移動到B

A -&> B

Step2.將最大的環從A移動到C

A -&> C

Step3.把除了最大的環之外的環,從B移動到C

B -&> C

三個環:

Step1.把除了最大的環之外的環,從A移動到B

A -&> C

A -&> B

C -&> B

Step2.將最大的環從A移動到C

A -&> C

Step3.把除了最大的環之外的環,從B移動到C

B -&> A

B -&> C

A -&> C

所以其實是這樣抽象成三個步驟的~

這個時候,可以放張圖了

(a)是初始狀態,也就是遞歸的起點,我們假設n=4, move(4,A,B,C)還是請參考現在最高的分的代碼哈~寫這個是幫助大家更清楚那個讓人壓力大的(「抽象」)兩個字,哈哈

&<這個函數要實現的功能是把n個環從A按照一定的規則,藉助B,移動到C&>

(b)是step1完成的時候的狀態,已經將所有的n-1,這裡也就是3個環從A挪到了B

&<第一處遞歸,move(n-1,A,C,B) 這個函數要實現將n-1個環從A,藉助C,移動到B&>

(c)是step2,此時需要將第n個,也就是第四個最大的環從A挪到C

& C")&>

(d)是step3,此時需要將B上面的n-1個環從B挪到C&<第二處遞歸&>

&<第二處遞歸,move(n-1,B,A,C) 這個函數要實現將n-1個環從B,藉助A,移動到C&>

Over~


我做了個動畫

三個盤子的漢諾塔你總會吧:

a_3=7

然後你移完發現左邊柱子下面又蹦出來一個盤子

好吧, 那就把中間的柱子看成目標柱

然後把最大的移到右邊, 然後就和搬三個一模一樣了

a_4=a_3+1+a_3

更多的話也是一樣的...

所以 a_n=2a_{n-1}+1


所以說一共就三步

  1. 把 n-1 號盤子移動到緩衝區
  2. 把1號從起點移到終點
  3. 然後把緩衝區的n-1號盤子也移到終點

所以寫成py代碼就是

def move(n,from,buffer,to):
if n==1:
print("Move",n,"from",from,"to",to)
else:
move(n-1,from,to,buffer)
move(1,from,buffer,to)
move(n-1,buffer,from,to)

  1. 要從a到b 那c就是緩衝 move(n-1,from,to,buffer)
  2. 要從a到c 那b就是緩衝 move(1,from,buffer,to)
  3. 要從b到c 那a就是緩衝 move(n-1,buffer,from,to)

毫無理解難度啊...

C++那不一樣的嗎

using namespace std;
#include &
#include &

void move (int n, char from, char buffer, char to){
if (n == 1) {
cout &<&< "Move" &<&< n &<&< " from " &<&< from &<&< " to " &<&< to &<&< endl; } else { move (n-1, from, to, buffer); move (1, from, buffer, to); move (n-1, buffer, from, to); } } //別打我, 我習慣不換行了, 反正現在有格式化工具...... //main()你自己寫吧

Mathematica的話...我會直接Fold一個迭代器

迭代器會自己去找合適的緩衝區...

這就是 Frame - Stewart 演算法

不過講道理這麼寫挺欠揍的...誰?給我 pull 這種代碼我直接打上去了2333

話說這個可視化我畫了4個小時...

Get["https://raw.githubusercontent.com/GalAster/Deus/master/Packages/_
_Raw/HanoiTower.wl", CharacterEncoding -&> "UTF8"]
HanoiShow@HanoiMove[10, Pillar -&> 5] // ListAnimate


漢諾塔永遠只有三步:

圖中是最常見的五層(五珠)漢諾塔,

其實幾層都是一樣,這裡設為n,

冰箱門永遠是漢諾塔上面的m=n-1層。

那麼問題來了,怎樣把冰箱門打開?

即:怎樣把圖中的1至4號串珠從A柱移動到B柱?

(三根柱子從左至右依次為A、B、C,五顆串珠從小到大依次為1到5)

這又變成了一道m層漢諾塔的問題(m=n-1)。

你可以繼續用把大象裝冰箱分幾步的思路

去考慮m層漢諾塔的解法。

推導下去最終就得到了一個兩層漢諾塔該怎麼移動的問題,

這個相信你閉著眼也知道該怎麼搞了。

【白雲大媽的冰箱理論其實蘊含了深刻的哲學道理 -_-!!!】

【不想看公式的話,往下翻有實操技巧】

……………公式解釋……………

關於漢諾塔的公式:

可以這樣理解:

其中

代表把冰箱門打開又合上,即完成兩次n-1層漢諾塔的過程,

+1 代表移動漢諾塔最下面一層,即把大象裝冰箱的過程。

冰箱門打開或者合上需要的步數都是一樣的,

都是完成一個m=n-1層漢諾塔的過程。

……………實操技巧……………

實際操作的時候有一個規律:

奇偶相關性

思考一下:

怎樣成為王健林那樣的有錢人?

我們需要先設定一個階段性目標:先掙它一個億。

好吧,這裡其實只是想引出個階段性目標的概念。

比如,

如果你想把5號串珠移動到C柱上,

那你的階段性目標是:先把3號串珠移動到C柱上。

那怎樣把3號串珠移動到C柱上呢?

你的階段性目標是:先把1號串珠移動到C柱上。

5是奇數,那麼階段性目標就是比當前目標小2的下一個奇數。

5→3→1,

這樣第一步該怎麼走就確定了。

階段性目標/奇偶相關性」和「冰箱理論」結合起來,

接下來的每一步也都知道該怎麼走了。

偶數也是同樣的道理,

6層漢諾塔的推理就是6→4→2。

配圖是臨時找了個頁游截的圖,

只是為了更加直觀。

之前隨手寫的答案,公式確實有問題,

使用LaTeX重新編輯了一下,

多謝評論區的朋友指正。


遞歸當然只能以遞歸的思路理解,把它展開純屬自討苦吃。

遞歸思路,說白了是如下三步:

1、對於問題N,如果N-1已經解決了,那麼N是否很容易解決。

甲:想上天?你先給我找個天一樣高的梯子!

乙:想要天一樣高的梯子?你先給我弄棵天一樣高的樹!

甲:想要天一樣高的樹?你先給我弄把能頂到天的鋸子!

……

舉例來說,如果要把一個N層漢諾塔從A搬到C,那麼:

如果前N-1層可以找別人搞定,咱只管搬第N層,會不會變得非常容易?

你看,這一下就簡單了:這時當它只有兩層就好了,先把前N-1層看作一個整體,把它搬到B;然後把最下面的第N層搬到C;然後再把前N-1層從B搬到C。

類似的,假如接到「搬前N-1層」這個任務的是我們,怎麼搬呢?

簡單,像前東家一樣,把前N-2層外包出去,我們只搬第N-1層——其實和前面討論過的「外包N-1層,只搬第N層」完全一樣嘛。

依此類推,一層層「外包」下去——我不管你們有多傷腦筋,反正只要你們把我外包給你的活幹了,我就能幹了我的活!

注意,這裡千萬別管「接到外包工作的傢伙有多傷腦筋」——丟給他就讓他頭疼去,我們就別再撿回來痛苦自己了。

這一步就是「遞推」。

注意這裡的搬法:搬第N層,就需要把前N-1層搬兩次,另外再把第N層搬一次;搬第N-1層,又需要把前N-2層搬兩次,然後再把N-1層搬一次,依此類推。

很容易知道,一共需要搬2^N-1次。

2、一步步遞推下去,終究會有個「包工頭」,接到「搬第一層」的任務。

第一層怎麼搬?

太簡單了,讓搬哪搬哪。

換句話說,到此,「遞推」就到了極限,簡單粗暴直接做就可以了。

3、既然第一層搬了,那麼第二層當然就可以搬了;第二層搬了,第三層又可以搬了……依次類推,直到第N層。於是問題搞定。

這一步就是「回歸」。

如上三步加起來,就是「遞歸」。

推而廣之,任何問題,不管規模為N時有多複雜,只要把N-1那塊「外包」給別人做之後,我們在這個基礎上可以輕易完成N,那麼它很可能就適合用「遞歸」解決。

那麼,怎麼最終確定它能不能用「遞歸」做呢?

看當N取1或2之類最簡情況時,問題是否可以解決——然後寫程序解決它。

容易看出,「遞歸」其實和「數學歸納法」的思路非常像:證明N=1時成立;證明若N=n-1成立,則N=n時也成立;如上兩步得證,則命題在n&>1時一定成立(n為自然數)。

你看,我們沒必要從1開始逐一驗證每個自然數,只要證明了「基礎條件」、再證明了「遞推條件」,大自然的規律會幫我們搞定一切。

換句話說,只要我們:

1、寫程序告訴電腦「如何分解一個問題」

2、寫程序告訴電腦「當該問題分解到最簡時如何處理」

那麼,「具體如何遞推、如何回歸」這個簡單問題就不要再操心了,電腦自己能搞定。

——寫出問題分解方法、寫出分解到最簡後如何解決,這是我們的任務;把問題搞定,是電腦的任務。這就是遞歸的魅力。

正是由於這種「我提供思路你搞定細節」的特點,「一切皆遞歸」的函數系語言才被稱為「聲明式編程」(而不是必須一步一步指導電腦如何做的「命令式編程」)。

這麼簡單輕鬆的事,非要自己吭哧吭哧一步步做出來——O(2^N)規模啊——難怪你們要陷入困境。


搞清楚遞歸只要搞清兩點:

  1. 結束條件
  2. 把問題規模縮小

在什麼是遞歸這個問題 李冰答主借用了網路上的一張圖片,非常形象,此處引用一下

出口條件就是圖中的小鯉魚,規模縮小是找個除了規模更小其他都一模一樣的自己

在漢諾塔問題中小鯉魚就是只有一個盤子的時候

但是如何讓自己規模縮小呢?

由於小的盤子不能在大的盤子上,最後一個盤子挪的時候前n-1個盤子必須已經被挪開,因此問題被分為三步,如下(手拙,望包涵):

經過這樣的分解,一個挪n個盤子的問題被轉化成了:兩次挪n-1個盤子的過程加一個直接move的過程。問題規模由n變為n-1,就可以繼續縮小直到找到小鯉魚——只剩一個盤子的情況。

用JS簡單實現了下,對照注釋和前面的解釋理解一下:

//把n個盤子借住helper從from柱子挪到to柱子上
function hanoi(n,from,to,helper){
//判斷是否找到我們的小鯉魚,找到我就直接返回,沒找到我就把自己規模縮小
if(n==1){
console.log("move a plate from "+ from + " to " + to);
return;
}
hanoi(n-1,from,helper,to);//把n-1個從from挪到helper上,藉助to
move(from,to);//把第n個盤子直接從from挪到to
hanoi(n-1,helper,to,from);//把n-1個從helper挪到from上,藉助to
}
//把一個盤子從一個柱子挪到另一個柱子上
function move(from,to)
{
console.log("move a plate from "+ from + " to " + to);
}

最後划下重點:對於遞歸問題,要抓住兩點:結束條件如何讓問題規模縮小問題。遞歸問題的正確性是根據數學歸納法來證明的,只要找到這兩點也就可以使用數學歸納法了。

特別注意分析問題的時候不要讓自己按著程序運行的順序鑽進去,這樣會很容易困惑和亂掉,盯准關鍵兩點,問題自然迎刃而解。


遞歸就是數學歸納法。

翻一翻以前用數學歸納法求解數列的題,想想數學歸納法的幾個步驟,在回過頭看看漢諾塔、斐波那契數列的函數代碼。

是不是以一毛一樣?


設f(n)表示n層漢諾塔要全部移走需要移動多少次盤子,有:

f(n) = 2f(n-1)+1

不就是另一個數列的遞推公式么……


首先,你有n個盤子,你想要把它們順時針移動,那麼你要做的是什麼呢??

逆時針移動前n-1個盤子,順時針移動第n個盤子,逆時針移動前n-1個盤子。

這三步。

逆時針移動同理。

那麼再看看basecase,你有兩個盤子,你想要把它們順時針移動。

則逆時針移動第一個盤子,順時針移動第二個盤子,逆時針移動第一個盤子。getit!

所以漢諾塔的每一步,其實都能分為三個小的步驟,包括對前面n-1個盤子的遞歸處理,對第n個盤子的處理,和對前面n-1個盤子的再次遞歸處理。

一共有兩種情況就是逆時針或者順時針移動。(把三個底座以三角形擺放)

不知道能不能幫到你。


以下是我的理解,有不正確的地方或錯別字歡迎指出/討論。

首先初始化這個問題:

設三根柱子分別為A、B和C,圓環個數為n且位於A,目標柱子是C。

然後抽象地看待這個問題:

從下往上看問題:假設把底盤n移動到C後,由於底盤n將不會再有任何移動,這時實際上就可以無視n了。於是第n-1個(初始狀態的倒數第二個)環成為新的底盤,將n-1移動到C後,同樣可以無視之。以此類推,不停地無視底盤、簡化圓環個數:n-2,n-3……剩5個圓環、4個、3個……最後其實就剩下2個環和1個環這兩種情況了。

  1. 對於1個環,直接A→C即可

  2. 對於2個環,A→B,A→C,B→C三步即可

整個問題,整體上抽象成這兩步。

對於上面把問題抽象出來的那段描述,對於n &> 1的時候,又可以抽象(換成「總結」這個詞會沒那麼大壓力嗎?)出這3步:

  1. 將底盤n以上的環(n-1個)移動到B

  2. 將底盤n從A移動到C

  3. 將B上的環(n-1個)移動到C

其中第1步和第3步的步數是一樣的,因為環的數量一樣(n-1個),只是目標的柱子不同而已。

實際上,這不斷無視底盤、不斷抽象的過程就是在進行遞歸調用了。

所以Python中的實現:

def hanoi(n,a,b,c):
if n==1: # 1個環的情況,也是遞歸的出口
print(a,"-&>",c)
else: # n &> 1時,用抽象出的3步來移動
hanoi(n-1,a,c,b) # 將n-1個環從a移動到b上
print(a,"-&>",c) # 將底盤從a移動到c上
hanoi(n-1,b,a,c) # 將b上的n-1個環移動到c上


這個問題最近挺困擾我,網上看了很多文章都在說用函數的角度去理解整個過程,確實是沒錯而且是一種聰明的辦法,但是有強迫症的我還是花了些時間去思考了一下遞歸每一步的運行,說一下個人的理解吧。 如有不正確以及不完善的,還請前輩指正。

首先貼上漢諾塔程序java代碼

假設三個柱子分別為a,b,c。hanoi(n,a,b,c)表示把n個盤子從a轉移到c。

1.當只有一個盤子時,很簡單,把它從a直接移動到c,也就是執行了n==1的情況。記該動作為move(a,c),代碼中直接使用列印輸出表示該動作。

2.當有兩個盤子時,即調用方法hanoi(2,a,b,c),理解為把2個盤子從a移動到c。分為3個步驟。

i.首先把最上面的盤子移動到b

ii.把下面的盤子移動到c

iii.把b的盤子移動到c。

為了便於理解遞歸,我們使用n==1時的動作,只是把柱子的編號進行一些變化。對於步驟i,動作為move(a,b),相當於調用方法hanoi(1,a,c,b),即交換b柱和c柱的編號,此時列印輸出a-&>b;對於步驟ii,不對自身進行調用,因為始終是由a至c,因此直接列印輸出a-&>c;對於步驟iii,動作為move(b,c),相當於調用hanoi(1,b,a,c),即交換a柱和b柱編號。因此對於2個盤子的情況,遞歸演算法可以寫為

hanoi(1,A,C,B);

System.out.println(A+"-&>"+C);

hanoi(1,B,A,C);

3.同樣對於3個盤子的情況,將上面兩個盤子視為一個大盤子,第一步將該大盤子轉移到b,相當於調用hanoi(2,a,c,b),即把兩個盤子從a移動到b。第二步將a上的最下面的盤子移動到c,執行注釋2對應的語句;最後,把b上的兩個盤子轉移到c,即調用hanoi(2,b,a,c)。因此對於三個盤子的代碼可以寫為

hanoi(2,A,C,B);

System.out.println(A+"-&>"+C);

hanoi(2,B,A,C);

由以上就很容易推出n個盤子的情況了。對於代碼的執行情況和基本思想是一樣。

分割線------------------

感謝有同學提出我的回答的錯誤所在,在此回頭看了一下,的確寫的亂七八糟,就把之前寫的刪了。現在我自己畫了一張圖,雖然有點丑,但是闡明了執行順序和過程。

箭頭表示實際的代碼語句執行順序。每一次調用hanoi()時,都會首先判斷n的值,若不為1,將當前參數入棧並將接下來的調用入棧,裡面的函數返回後出棧繼續執行接下來的語句,這一點在圖片中的舉例有詳細說明。不懂的可以查一下函數調用過程其實就是棧的操作,對於理解遞歸很有用。雖然在使用遞歸時並不需要糾結在這些細節,只需要將功能視作函數操作就行,但是還是希望能有助於理解,畢竟熟練運用遞歸併非易事。第一次寫這方面的內容,如果有分析的錯誤還請指正。


推薦看一下 3Blue1Brown 關於漢諾塔的視頻,相信大家會受益匪淺。

傳送門:https://youtu.be/2SUvWfNJSsM

國內用戶關懷:av7398130


如果你有耐心翻到這裡看到這個回答,相信大部分前面的回答並沒有讓你完全明白漢諾塔的遞歸演算法應該怎麼理解,那麼我希望這個答案會是你瀏覽的最後一個。ps:篇幅稍微有點長,請耐心閱讀,相信我,會有價值的。(畢竟是個人理解,若其中有不正確的地方望大家多多指正哈)

要用程序來解決這個問題,我們先定義一個移動函數:move(移動數,開始柱,中轉柱,目標柱),例如 move(2,A,B,C) 表示將2個盤子從A柱(開始柱)藉助B柱(中轉柱)移動到C柱(目標柱)。

關於開始柱,中轉柱,目標柱這三個概念可以用移動過程中的某個狀態來理解,看下面一張圖應該就能明白:

以上圖的三層漢諾塔為例,開始柱指的是開始狀態時存放所有盤子的柱子,中轉柱指的是中間狀態時暫時存放n-1個(三層就是3-1個)盤子的柱子,目標柱指的是盤子最終要移動到的柱子。這裡需要注意,開始柱,中轉柱,目標柱並不是一成不變的,而是會根據層次的不同而改變。(如果這裡暫時不能理解的話,先讀下去,再回頭你就能明白了)。

接著我們分情況討論一下盤子的移動:

情況一:當盤子只有1個(調用 move(1,A,B,C))

當盤子只有一個的時候,只要直接將盤子從開始柱移動到目標柱就可以了,並沒有中間狀態(即不用藉助中轉柱),在move方法中可以用一句話表示該移動動作 print("A---&>C");

情況二:當盤子有2個(調用 move(2,A,B,C))

這個情況分三個步驟進行:

step1. 把除了最大的盤子之外的盤子從A移到B

A---&>B (開始柱---&>中轉柱) 【相當於調用 move(1,A,C,B)】

step2. 把最大的盤子從A移到C

A---&>C (開始柱---&>目標柱) 【相當於調用 move(1,A,B,C)】

step3. 把除了最大的盤子之外的盤子從B移到C

B---&>C (中轉柱---&>目標柱) 【相當於調用 move(1,B,A,C)】

我想對於以上兩個情況大家應該是沒有什麼疑問的,是可以確定的。然後我們來看三層的情況:

情況三:當盤子有3個(調用 move(3,A,B,C))

這個情況同樣分三步進行:

step1. 把除了最大的盤子之外的盤子從A移到B(注意對於這個步驟來說此時A為開始柱,C為中轉柱,B為目標柱,這樣才能完成把最上面的2個盤子從A---&>B的任務)

A---&>C (開始柱---&>中轉柱) 【相當於調用 move(1,A,B,C)】

A---&>B (開始柱---&>目標柱) 【相當於調用 move(1,A,C,B)】

C---&>B (中轉柱---&>目標柱) 【相當於調用 move(1,C,A,B)】

step2. 把最大的盤子從A移到C(對於這個步驟來說此時A為開始柱,B為中轉柱,C為目標柱,這樣才能把最大的盤子從A---&>C)

A---&>C (開始柱---&>目標柱) 【相當於調用 move(1,A,B,C),即直接執行 print("A---&>C")】

step3. 把除了最大的盤子之外的盤子從B移到C(注意對於這個步驟來說此時B為開始柱,A為中轉柱,C為目標柱,這樣才能完成把處於step2中的中轉柱的2個盤子從B---&>C的任務)

B ---&> A (開始柱---&>中轉柱) 【相當於調用 move(1,B,C,A)】

B ---&> C (開始柱---&>目標柱) 【相當於調用 move(1,B,A,C)】

A ---&> C (中轉柱---&>目標柱) 【相當於調用 move(1,A,B,C)】

情況三的描述可能一下子不是那麼好理解,但是大家應該能發現情況三的step1和step3的形式和整整個情況二的形式很像吧?而且要注意到分析的層次不相同時,開始柱,中轉柱,目標柱是不一樣的。對於step1來說中轉柱是C,對於step3來說中轉柱是A,對於整個情況三來說中轉柱是B。

前面我們已經確定了情況二調用的函數是 move(2,A,B,C),其等價於

A---&>B

A---&>C

B---&>C

這三步,然後情況三的step1是

A---&>C

A---&>B

C---&>B

這三步,跟情況二的形式是一樣的,根據前面情況二的轉化,那這三步就可以轉化成函數 move(2,A,C,B)

情況三的step3同理,做轉化就成了函數 move(2,B,A,C)

而情況三的step2可以直接用一句 print("A---&>C") 來代替 move(1,A,B,C)

所以整個情況三就可以這樣來表示:

move(2,A,C,B) //step1. 把除了最大的盤子之外的盤子從A移到B

print("A---&>C") //step2. 把最大的盤子從A移到C

move(2,B,A,C) //step3. 把除了最大的盤子之外的盤子從B移到C

我們又知道情況三調用的函數是 move(3,A,B,C),所以以上三行代碼其實就等價於函數 move(3,A,B,C)。

來到這裡應該就可以發現一點端倪了,情況四(4層)調用的函數是 move(4,A,B,C),其用偽代碼表示就是

move(3,A,C,B) //step1. 把除了最大的盤子之外的盤子從A移到B

print("A---&>C") //step2. 把最大的盤子從A移到C

move(3,B,A,C) //step3. 把除了最大的盤子之外的盤子從B移到C

對此有懷疑的小夥伴可以自己寫出情況四的每步具體步驟然後再做轉化,限於篇幅這裡不再列出。

那其實可以總結出:對於n(n&>1)層漢諾塔,調用函數 move(n,A,B,C)來遞歸解決該問題,該函數執行的是

move(n-1,A,C,B) //step1. 把除了最大的盤子之外的盤子從A移到B

print("A---&>C") //step2. 把最大的盤子從A移到C

move(n-1,B,A,C) //step3. 把除了最大的盤子之外的盤子從B移到C

然後有了以上的理解之後,我們就可以嘗試寫出解決該問題的代碼了,Python實現:

def move(n,A,B,C):
if n==1: # 1個盤子,直接列印出移動動作
print(A,"---&>",C)
else: # n &> 1時,用抽象出的3步來移動
move(n-1,A,C,B) #step1. 把除了最大的盤子之外的盤子從A移到B
print(A,"---&>",C) #step2. 把最大的盤子從A移到C
move(n-1,B,A,C)#step3. 把除了最大的盤子之外的盤子從B移到C

so,讀到這裡,我大膽猜測大部分人心裡應該明白得七七八八了,如果說我猜錯了,別急,我們還可以從函數的角度來理解這個問題。

或許有一部分人知道這個函數如何編寫,也似懂非懂的了解這個函數注釋的意義,但是可能會糾結為什麼函數寫成這樣子就能詳細地列出具體的移動步驟(我就是這部分人啊,研究這個問題沒少鑽牛角尖),下面就跟大家分享下我的另外一個見解。

well,假設現在我們要移動一個4層的漢諾塔,從A---&>C:

那麼我們必須先將上面3個盤子移動到B,然後再將最大的盤子從A移動到C,接著將在B上的三個盤子從B移動到A才能達到目的。這個過程的關鍵在於第一步,怎麼把上面的3個盤子從A移動到B?

OK,前面說要移動4個就必須先移動上面3個,所以這裡我們要移動3個就必須移動上面2個,但是並不是移動到B哦,因為B已經被我們預定好了要用於存放前面移動的3個盤子,如果是將上面2個盤子移動到B,那根據遊戲規則第三個盤子就沒辦法放進去B了,這樣子就沒辦法完成B存放3個盤子的預定目標,所以要移動的上面兩個是計劃放在C上。

關鍵狀態

那問題接著就到了該怎麼移動上面2個盤子,這時你可能會想要先移走最上面的盤子(這時候要移到B,因為C被我們預定了要存放2個盤子,且B這時候是沒有盤子的),沒錯,那最上面的盤子怎麼移?直接移唄怎麼移,最上面的盤子都沒限制的。所以要移動1個盤子的時候,直接移動: A---&>B,注意,這個步驟中A為開始柱,B為目標柱。

這時你已經移開最上面的盤子了,就可以把第二盤子移走了,A---&>C,然後把最小的盤子放到第二個盤子上,B---&>C,這樣就完成了移動2個盤子的任務了。

總結下移動兩個盤子的步驟:

A---&>B

A---&>C

B---&>C

這三個步驟就是move(2,A,B,C)所做的事情,是可以詳細列出每步移動的動作的。

既然最上面的2個盤子都調用move(2,A,B,C)移開了,那麼第3個盤子自然也可以從A---&>B了,之後再把放在C上面的2個盤子從C移動到B上就完成了移動上面3個盤子的任務了。前面的move(2,A,B,C)函數既然可以將2個盤子從A藉助B移動到C並列出詳細的移動動作,那麼move(2,C,A,B)也就能將放在C上的2個盤子從C藉助A移動到B並列出詳細的移動動作,如此說來,移動3個盤子的步驟就可以總結如下了:

move(2,A,B,C)

A---&>B

move(2,C,A,B)

這三個步驟就是move(3,A,C,B)所做的事情,因為我們已經證明move(2,A,B,C)和move(2,C,A,B)是可以詳細列出移動2個盤子時每步移動的動作的,而中間的A---&>B是一步顯而易見的移動動作,所以可以確定move(3,A,C,B)是能列出每步的移動動作的。

然後根據同樣的分析,最上面的3個盤子都移開了,接著只要將第4個盤子從A---&>C,然後將放在B上的3個盤子移動到C上就完成全部任務了。前面我們已經證明move(3,A,C,B)能將3個盤子從A藉助B移動到C並且列出詳細的移動動作,那麼move(3,B,A,C)也能將3個盤子從B藉助A移動到C並列出每步的移動動作,這樣,移動4個盤子的步驟就出來了:

move(3,A,C,B)

A---&>C

move(3,B,A,C)

這三個步驟就是最開始move(4,A,B,C)函數所做的事情,因為我們已經證明move(3,A,C,B)和move(3,B,A,C)是可以詳細列出移動3個盤子時每步移動的動作的,而中間的A---&>C是一步顯而易見的移動動作,所以可以確定move(4,A,B,C)是能列出每步的移動動作的。

需要說明的是,從xx藉助xx移動到xx這樣的說法在上面出現了很多次,「從」後面代表的就是開始柱,「藉助」後面代表的是中轉柱,「移動到」後面代表的是目標柱。你也能注意到到 開始柱,中轉柱,目標柱 在不同層級下是不一樣的。

根據以上的分析,希望大家就能明白move(n,A,B,C)遞歸函數的意義了(忘記函數具體內容的往前翻哦)可能有些啰嗦,但是已經儘力表達了我想表達的東西,希望能幫上大家一點忙吧,溜了~

----------------------------------- 我是分割線么?------------------------

ps: 大家都默默收藏了答案,不如再順手點個贊讓更多人看到?hiahiahia~


代碼如下:

首先,題主你需要先理解n=2的情況,非常簡單:

n=2時:

  1. 執行move(1,a,c,b),列印出A--&>B;

  2. 執行A--&>C;

  3. 執行move(1,b,a,c),列印出B--&>C;

因此n=2時,相當於是將A中的兩個盤子按照規則搬到了C中。

n=3時:

  1. 執行move(2,a,c,b),帶入我們之前所理解的結果,即將A中的上面2個盤子按照規則搬到了B中,並且列印出過程;

  2. 將A中最下面的盤子搬到c中,列印A--&>C;

  3. 執行move(2,b,a,c),即將B中的2個盤子按規則搬到C中,並且列印出過程。

n=4時程序基於n=3上運行,一層套一層,這就是遞歸。


用我們當年OI競賽敎練的話來說:你就是個將軍!(雖然這句話原本是在講動態規劃的時候說的,不過用在這裡也毫不違和)

你就是個將軍,你的任務就是要把n個漢諾塔從第一個搬到第三個柱子去。

那麼你祇需要喊一個手下,讓他先幫你把n-1個塔從一號柱搬到二號柱,然後你把一號柱最底下的塔搬到三號柱上,再讓這個手下幫你把二號柱上剩餘的塔搬到三號柱,這樣你的任務就完成了。

至於你的手下怎麼做,那是他的事情,你無需操心。他可能一次性也搬不完,那麼他再去找手下來做,方法跟你一樣。

當然,如果有個手下被分配到移動最小的那個塔時,他就直接移動就行了,不需要再請手下了。


遞歸演算法詳解第4.2節


謝沒邀,看到了這個問題就進來逛一下。

漢諾塔問題是一種很經典的實際遞歸問題。

對於遞歸,我的看法就是在人腦在即使藉助數學也很難想像解決問題的全部細節過程時所採用的方法。

最高票 @YIHE陳 解釋得很好,但是只解釋清楚了挪動所需的次數是T_{total}=2^ {n}-1  的原理

沒有解釋清楚每一次挪動到底是怎麼來的。

在此結合個人看法敘述下:

---------------------------------------------------------------------------------------

問題如下:

假設一共有n個盤子,由1號盤到n號盤依次增大。

記作:

P_{1},P_{2},P_{3},P_{4}.........P_{n-2},P_{n-1},P_{n}

其中P_{1}最小,P_{n}最大。

一共三根柱子;A(start)柱子,B(middle)柱子,C(end)柱子。

注意A,B,C後面的括弧里的內容,start代表初始放置柱子,middle代表中轉柱子,end代表目標柱子。

問題開始時,start柱子上n個盤子從上到下按從小到大的順序排列。

middle柱子和end柱子上沒有圓盤。

當問題結束時,start柱子和middle柱子上沒有圓盤,而end柱子上n個盤子從上到下按從小到大的順序排列。

---------------------------------------------------------------------------------------

問題的解法一共三大步驟:

步驟1:將A(start)柱子上P_{1}.........P_{n-1}以某種方法全部放到B(middle)柱子上(此時A(start)柱子只有P_{n},middle柱子上有P_{1}........P_{n-1},而C(end)柱子上沒有盤子)

步驟2:將A(start)柱子上僅存的P_{n}挪到空無一物的C(end)柱子上。

步驟3:將B(middle)柱子上剩餘的P_{1}......P_{n-1}全部挪到C(end)柱子上。

---------------------------------------------------------------------------------------

就是這三步。第二步最好理解,

因為如果P_{1}......P_{n-1} 不全在B(middle)柱子上的話,P_{n} 也就無法從A(start)柱子挪動到C(end)柱子上。

同時既然P_{1}......P_{n-1}都在B(middle)柱子上,那麼他們的排列順序一定是從上到下從小到大。

---------------------------------------------------------------------------------------

下面詳解步驟1和步驟3

對於步驟1:完全可以看作是將n-1個盤子由A(start)盤子經由C(middle)盤子挪動到B(end)盤子上。

A仍是初始放置柱子,但此時對於這n-1個盤子,中轉柱子B變成了目標柱子B(end),而C柱子成為了中轉柱子。

對於步驟3:此時前n-1個盤子都在B柱子上,要將他們挪動到C(end)柱子上,此時B柱子對於這n-1個盤子是初始位置盤,是B(start)。而A柱子成為了中轉柱子,是A(middle)。

所以明白了吧?

步驟1和步驟3都分別是一次獨立的漢諾塔問題,區別在於:

常規漢諾塔問題:n個盤子,從A(start)柱子,經由B(middle)柱子,到C(end)柱子。

步驟1的漢諾塔問題:n-1個盤子,從A(start)柱子,經由C(middle)柱子,到B(end)柱子。

步驟3的漢諾塔問題:n-1個盤子,從B(start)柱子,經由A(middle)柱子,到C(end)柱子。

---------------------------------------------------------------------------------------

而步驟1又可以變成:

步驟1.1:n-2個盤子,從A(start)柱子,經由B(middle)柱子,到C(end)柱子。

步驟1.2:最大的盤子P_{n-1} 從A(start)柱子,一步,到B(end)柱子

步驟1.3:n-2個盤子,從C(start)柱子,經由A(middle)柱子,到B(end)柱子。

---------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------

步驟1和步驟3均包含2^{n-1}-1 次挪動,步驟2包含1次挪動。總體是2^{n} -1次挪動

---------------------------------------------------------------------------------------

以下是用java寫的程序,幫助你理解

package number;
import java.util.Scanner;
public class Hannuota {
int n;
char start;
char middle;
char end;
public void hanoi(int n,char start,char middle,char end){
this.n=n;
this.start=start;
this.middle=middle;
this.end=end;
if(n==1){
move(start,end);
}else{
hanoi(n-1,start,end,middle);
//將在A(start)柱子上的前n-1個盤子移動到B(end)柱子上
move(start,end);
//將在A(start)柱子上的最大的n盤移動到C(end)柱子上
hanoi(n-1,middle,start,end);
//將在B(start)柱子上的前n-1個盤子移動到C(end)柱子上
}
}
public void move(char start,char end){
System.out.println(start+"-&>"+end);
}
public static void main(String args[]){
int n;
Scanner scan=new Scanner(System.in);
System.out.println("how many Hanoi plate do you have:");
n=scan.nextInt();
Hannuota han=new Hannuota();
han.hanoi(n, "A", "B", "C");
}
}

輸出的計算結果:

how many Hanoi plate do you have:
3
A-&>C
A-&>B
C-&>B
A-&>C
B-&>A
B-&>C
A-&>C


這個程序本身並不複雜,但理解起來卻不容易。我的程序是這樣

這個程序需要深刻的理解遞歸函數的特點,也就是從複雜的情況推翻一般情況,這道題的一般情況就是盤子數為1。同時也要理解棧的先進後出的特點。

我想了很久這個程序運行的過程。終於有點眉頭。當盤子為3時,程序的運營順序是

結合棧與遞歸的原理,就不難去理解漢諾塔的編程了。


推薦閱讀:

為什麼近幾年浙江省信息學競賽這麼厲害?
C語言中,單個&和|,與成雙的&&和||,區別在哪裡?
強人工智慧的產生是否離不開數理邏輯的支撐?
求幫助,Python閉包和返回函數問題?
如何紮實地學習Lisp?

TAG:思維 | 編程 | C | 遞歸 |