化繁為簡 經(jīng)典的漢諾塔遞歸問題 in Java

問題描述
在世界中心貝拿勒斯(在印度北部)的圣廟里,一塊黃銅板上插著三根寶石針。印度教的主神梵天在創(chuàng)造世界的時候,在其中一根針上從下到上地穿好了由大到小的64片金片,這就是所謂的漢諾塔。不論白天黑夜,總有一個僧侶在按照下面的法則移動這些金片:一次只移動一片,不管在哪根針上,小片必須在大片上面。僧侶們預(yù)言,當(dāng)所有的金片都從梵天穿好的那根針上移到另外一根針上時,世界就將在一聲霹靂中消滅,而梵塔、廟宇和眾生也都將同歸于盡。
扯遠了,把這個問題簡單描述下有A,B,C三根柱子,將A柱上N個從小到大疊放的盤子移動到C柱,一次只能移動一個,不重復(fù)移動,小盤子必須在大盤子上面。
問一共需要移動多少次,步驟是什么?



解決思路
讓我們從簡單的情況下開始考慮
首先我說明幾個數(shù)學(xué)符號,我自己瞎編的,為了方便表示問題,不用打那么多字
A -> B
箭頭意思代表是從 A柱 移動到 B柱 ,每次移動都是移動最上面的那一塊

n = 1
只有一個盤子的時候,很簡單,直接移就是了,用數(shù)學(xué)符號記錄一下代表 A -> C

n = 2
如果有兩個盤子,就分為三步
1.先將最上面的盤子也就是(從下往上數(shù))第2個盤子,先移動到B 記為 A -> B
2.然后將第一個盤子移動移動到C , A -> C
3.最后將 B柱子上的盤子 移動到 C柱 , B -> C

n = 3
如果有三個盤子,就分為7步,這里就不打字了,太累,用數(shù)學(xué)符號表示

A -> C

A -> B

C -> B

A -> C

B -> A

B -> C

A -> C



額,不要擺出一副黑人問號臉...,自己隨便拿三個道具擺一擺就知道了
當(dāng)你擺一擺的時候就知道,我知道了,一個盤子移動1次,二個盤子移動3次,三個盤子移動7次,四個盤子移動15次,N個盤子移動 (2^n - 1) 次!
恭喜你答對了!數(shù)學(xué)歸納法找規(guī)律不需多久就可以找到規(guī)律,但還有問題是究竟要怎么移呢?... 以及用程序怎么解決呢?
n = n....
抽象出這個問題
將問題抽象出來并且轉(zhuǎn)化為數(shù)學(xué)模型或者公式,是解決現(xiàn)實生活中復(fù)雜問題的一個很好的解決辦法,例如谷歌的翻譯,大家都覺得很智能,自然語言的翻譯從20世界60年代就開始研究了,具體細(xì)節(jié)這里不是重點,最后的解決思路是基于統(tǒng)計模型來解決的,也就是說,困擾了很多年的問題最后是抽象成一個概率論公式得以解決,事實上,眾多復(fù)雜的問題最后進行抽象其實就是幾個流程和公式而已,下面就以這個哈諾塔問題為例。

當(dāng)有N個盤子的時候,似乎很難去想具體怎么實現(xiàn),既然這么難想,就不用去想,首先這個問題,中有三個柱子,A,B,C,這里也太具體了,抽象一下,怎么抽象呢?假如這個問題只有2根柱子,你能完成嗎?廢話,肯定不行啊,我還需要一個柱子來輔助移動,所以這里的A,B,C三個柱子就抽象成,起始柱,中間柱,目標(biāo)柱,這里用from,mid,to來表示

ok,現(xiàn)在是盤子的數(shù)量抽象成了n,柱子也抽象成了,from,mid,to 三種柱子,下面對過程進行抽象,將n個盤子從 from 柱子 移動到 to 柱 ,其實總體來看是三步

將n-1個盤子從 起始 柱 移動到 中間 柱
將第n個盤子從 起始 柱 移動到 目標(biāo) 柱
將第n-1個盤子從 中間 柱 移動到 目標(biāo) 柱 這里你可能會說了,我靠,不是只讓移動一個盤子的嗎,你這第1步和第3步移動了n-1個盤子啊...,對,所以我這里的過程說的是抽象的過程,也就是說不管具體的實現(xiàn)細(xì)節(jié)是怎樣的,要達成所有的盤子都從A->C的效果,中間一定是有一步是達到這個效果的,就好比你從北京去紐約,假設(shè)只有一條國際航班,要經(jīng)過巴黎,那我就可以說你從北京去紐約,只有兩步,第一步是去巴黎,第二步是從巴黎去紐約,這里的道理是同樣的。

那么現(xiàn)在將上面的第1步怎么實現(xiàn)呢?同樣抽象,只要將n 替換成 n-1 即可,第三步也是同理

將n-2個盤子從 起始 柱 移動到 中間 柱
將第n-1個盤子從 起始 柱 移動到 目標(biāo) 柱
將第n-2個盤子從 中間 柱 移動到 目標(biāo) 柱

代碼實現(xiàn)
那這個過程,用程序來抽象就是 一個Plate方法,接受四個參數(shù),n,from,mid,to,四個參數(shù)的意思分別是
n 代表要移動幾個盤子
from 代表起始柱的名字
mid 代表借助的中間柱的名字
to 代表目標(biāo)柱的名字

方法的 作用是 將 n 個盤子 從 from 移動到 to
代碼如下

/**
     * 將n個盤子從 from 移動到 to
     */
    public static void movePlate(int n, String from, String mid, String to) {
        /* 如果只有一個盤子,就直接從 from 移動到 to */
        if (n <= 1) {
            System.out.println(from + " -> " + to);
            return;
        }

        /* 1.將 n-1 個盤子 從 from 移動到 mid */
        movePlate(n - 1, from, to, mid);

        /* 2.將第 n 個盤子 從 from 移動到 to */
        System.out.println(from + " -> " + to);

        /* 3.將 n-1 個盤子 從mid 移動到 to */
        movePlate(n - 1, mid, from, to);
    }

你可以發(fā)現(xiàn)除去注釋,真正的代碼只有5行,就將這個問題給解決了,再次提醒這里的from , mid ,to 是形參,代表的是起始住,中間柱,和目標(biāo)駐,不是具體的哪一個柱子,所以在第12行,因為第一步是將N-1個移動到中間柱,所以參數(shù)時from,to,mid,第18行將n-1個從中間柱移動到目標(biāo)駐,所以參數(shù)時mid,from,to,中間的參數(shù)就是需要借助的柱子。

下面測試一下代碼,這里根據(jù)題目把from,mid,to起個名分別是A,B,C,那執(zhí)行這個方法就是將3個盤子從A移動到C

public static void main(String[] args) {
        movePlate(3, "A", "B", "C");
    }

n = 3 的時候

A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

Process finished with exit code 0

發(fā)現(xiàn)和上面人為思考的結(jié)果是一樣的哦

當(dāng)n = 4的時候

A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
A -> C
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C

Process finished with exit code 0

一共是15步,也沒有問題,再多的我就不測了,有興趣的自己試試按照上面的打印結(jié)果來進行操作

推算次數(shù)

利用遞歸的方法同樣可以很容易的寫出計算次數(shù)的方法

public int countMovePlate(int n) {
        if (n <= 1) return 1;
        return countMovePlate(n - 1) + 1 +countMovePlate(n-1);
    }

那問題來了,還能優(yōu)化嗎?

上文說到人為觀察,利用數(shù)學(xué)歸納法可以得出需要的次數(shù)是 (2^n - 1) 次,那么這個數(shù)究竟是怎么得到呢?
先把上面的程序復(fù)制下來,進行觀察

/* 1.將 n-1 個盤子 從 from 移動到 mid */
        movePlate(n - 1, from, to, mid);

        /* 2.將第 n 個盤子 從 from 移動到 to */
        System.out.println(from + " -> " + to);

        /* 3.將 n-1 個盤子 從mid 移動到 to */
        movePlate(n - 1, mid, from, to);

核心代碼就三行,假設(shè)moveplate這個方法需要移動的次數(shù)為 (a_n) 次,那么上面的這三行需要移動的次數(shù)就應(yīng)該是 [ a_n = a_{n-1} + 1 + a_{n-1} ]

第一步是 (a_n) ,第二步是固定的1次,第三步又是 (a_n) ,然后當(dāng) n = 1的時候 (a_1 = 1) ,再總結(jié)整理一下就成了

[ a_n=\left{ \begin{aligned} 1, n = 1\ 2a_{n-1} + 1,n > 1 \end{aligned} \right. ]

有沒有夢回高中的趕腳,這是一個很簡單的變形等比數(shù)列,我們讓兩邊都加上1

[ a_n + 1 = 2a_{n-1} + 1 + 1 ]

也就是

[ a_n + 1 = 2a_{n-1} + 2 ]

再提取一下

[ a_n + 1 = 2(a_{n-1} + 1) ]

兩邊都除以 ((a_{n-1} + 1))

于是就成了

[ \frac {a_n + 1}{a_{n-1} + 1} = 2 ]

那接著將這個公式一直寫豎式將他們相乘

[ \frac {a_n + 1} {a_{n-1} + 1} = 2 ]

[\frac {a_{n-1} + 1}{a_{n-2} + 1} = 2 ]

[\frac {a_{n-2} + 1}{a_{n-3} + 1} = 2 ]

[\vdots]

[\frac {a_2 + 1}{a_1 + 1} = 2 ]

接著約分, Markdown LaTex公式的刪除線找了半天都沒找到,知道的麻煩告知一下.

約分結(jié)果是

[\frac {a_n + 1}{a_1 + 1} = 2^{n-1} ]

接著將前面的 (a_1 = 1) 代入,于是

[\frac {a_n + 1}{2} = 2^{n-1} ]

再整理一下

[a_n = 2^n - 1 , n > 1]

所以說

[ a_n=\left{ \begin{aligned} 1, n = 1\ 2^n - 1 , n > 1 \end{aligned} \right. ]

將n =1 代入 n > 1 的情況,也是成立的,因此

[a_n = 2^n - 1 ]

所以經(jīng)過推導(dǎo)之后java代碼如下,因為涉及到 (2^n) 這種運算,可以使用移位符,這樣底層移動速度很快

代碼如下

public static int countMovePlate(int n) {
        return n >= 1 ? (1 << n) - 1 : 0;
    }

結(jié)論

最終我們解決漢諾塔的移動順序與統(tǒng)計次數(shù)的代碼如下,可以看出并不需要幾行代碼就解決了問題

/* 打印出移動順序 */
    public static void movePlate(int n, String from, String mid, String to) {
        if (n <= 1) {
            System.out.println(from + " -> " + to);
            return;
        }
        movePlate(n - 1, from, to, mid);
        System.out.println(from + " -> " + to);
        movePlate(n - 1, mid, from, to);
    }
    
    /* 返回需要移動的次數(shù) */
    public static int countMovePlate(int n) { return n >= 1 ? (1 << n) - 1 : 0;}

java學(xué)習(xí)交流群:669823128 禁止閑聊,非喜勿加

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容