1. 程式人生 > >漢諾塔問題——遞迴(時隔9個月,終於懂了)

漢諾塔問題——遞迴(時隔9個月,終於懂了)

記得我第一次做漢諾塔這道題時,是2017年11月。當時,我坐在山大青島校區圖書館3樓,不知怎麼地,看到了這個題。

然後,就思考了一整天,233

當然,悲劇就是,我當時花了一天的時間還是沒有真正理解這道題遞迴的思路。

如今,我終於懂了,嘿嘿嘿。

關於遞迴: 一定不要試圖跟蹤大型遞迴的過程! 要寫出遞迴,關鍵就是找出遞迴的遞迴方程式: 也就是說,要完成最後一步,那麼最後一步的前一步要做什麼。

關於遞迴:

(1)在求f(n, other variables)的時候,你就預設f(n -1, other variables)已經被求出來了——至於怎麼求的,這個是計算機通過回溯求出來的。

PS:這裡用到了一種叫做棧(stack)的先進後出的資料結構,所以遞迴輸出的答案一般是自下而上的。

(2)遞迴和二叉樹是密切相關的。可以嘗試通過二叉樹的資料結構來理解遞迴是如何將一個問題拆分成若干子問題,求解再回溯的。這裡可以參考以下快速排序(QuickSort)的過程(快速排序的核心思想是分治,分治即分而治之,通過遞迴將原問題分解為若干容易求解的子問題,再通過遞迴將這些子問題聯絡起來並向二叉樹的上層回溯,最終求解出原問題)

遞迴的關鍵有兩個:

(1)遞迴的結束條件(不寫會死迴圈,TLE)

(2)遞迴最後一層和其他有關係的層的關係怎樣用非遞迴函式來表達

比如:斐波納契亞數列,(1)當n==1和n==2的時候f(n)=1,這就是遞迴的終止條件。給了終止條件,計算機才能進行求解子問題並回溯,最終求出f(n)

對於這個漢諾塔問題,在寫遞迴時,我們只需要確定兩個條件:

1.遞迴何時結束?

2.遞迴的核心公式是什麼?即:

怎樣將n個盤子全部移動到C柱上?

即:若使n個盤子全部移動到C柱上,上一步應該做什麼?

下面正式進入該題:

漢諾塔問題是一個經典的問題。漢諾塔(Hanoi Tower),又稱河內塔,源於印度一個古老傳說。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,任何時候,在小圓盤上都不能放大圓盤,且在三根柱子之間一次只能移動一個圓盤。問應該如何操作?

下面我們來寫遞迴函式。

首先,題目要求求的是如何操作,那麼我們就必須寫一個輸出操作語句的函式。

這個操作語句必須說明:第幾步將哪個盤子從哪個柱子移動到哪個柱子上(這樣人類才知道怎樣移動盤子嘛)

這裡,我們定義這個函式的函式名為move。

接下來,我們來確定這個函式的引數列表。

顯然,為了說明第幾步將哪個盤子從哪個柱子移動到哪個柱子上,我們引數列表至少應該包含:

id,表示被移動的盤子的序號。

from,表示從哪個柱子上移動這個編號為id的盤子

to,表示移動到哪個柱子上

那麼這個函式的函式頭就確定了:

void move(int id, char from, char to) // 列印移動方式:編號,從哪個盤子移動到哪個盤子

那麼函式體呢?

唯一的難點就是如何記錄這是操作的第幾步。

注意到,每次操作必須輸出移動方式且僅能輸出一次,那麼顯然,我們已經printf的當前總數不就是第幾次操作了嘛

我們開一個全域性變數用於記錄printf的次數即可

所以函式體中就只有這一個語句:

printf ("step %d: move %d from %c->%c\n", ++cnt, id, from, to);

合併起來就是:

void move(int id, char from, char to) // 列印移動方式:編號,從哪個盤子移動到哪個盤子
{
    printf ("step %d: move %d from %c->%c\n", ++cnt, id, from, to);
}

敲黑板:

遞迴函式怎麼寫呢?

我們先來想一下我們人類應該怎樣操作吧。

我們每次操作都會這樣問自己:我們需要將哪個柱子上的多少個盤子通過哪個柱子移動到哪個柱子上呢?

我們必須也只能用這麼幾個引數:

需要移動的盤子的總數,3個柱子。

所以函式頭為:

void hanoi(int n, char x, char y, char z)

其中,n代表盤子總數,x,y,z代表柱子

hanoi(n, x, y, z)的意思就是:將n個在x柱子上的盤子通過y這個柱子移動到z這個柱子上。

那不就完了!

hanoi(n, 'A', 'B', 'C')就是這道問題的答案!

那麼這一步的前一步是什麼?

記住了,在求解f(n, other variables)的時候,我們直接預設f(n - 1, other variables)已經完了就可以了!這個在前面已經解釋過了,在此不再鰲述。

我們將n-1個盤子當作一個整體:這就是類似於分治求解子問題的思想

那麼,前一步也就是f(n - 1, other variables)顯然是先將n -1 個在A柱子上的盤子通過C柱移動到B柱上,再將在A柱子上的編號為n的盤子移動到C柱上,再將B柱子上的n-1個盤子通過A柱移動到C柱上,over

C++程式碼如下:

void hanoi(int n, char x, char y, char z)
{
    if (n == 0)
        return;
    hanoi(n - 1, x, z, y);
    move(n, x, z);
    hanoi(n - 1, y, x, z);
}

漢諾塔完整程式碼:

#include <iostream>
#include <cstdio>
using namespace std;

int cnt;

void move(int id, char from, char to) // 列印移動方式:編號,從哪個盤子移動到哪個盤子
{
    printf ("step %d: move %d from %c->%c\n", ++cnt, id, from, to);
}

void hanoi(int n, char x, char y, char z)
{
    if (n == 0)
        return;
    hanoi(n - 1, x, z, y);
    move(n, x, z);
    hanoi(n - 1, y, x, z);
}

int main()
{
    int n;
    cnt = 0;
    scanf ("%d", &n);
    hanoi(n, 'A', 'B', 'C');
    return 0;
}

使用者友好版:

#include <iostream>
#include <cstdio>
using namespace std;

int cnt;

void move(int id, char from, char to) // 列印移動方式:編號,從哪個盤子移動到哪個盤子
{
    ++cnt; // 記錄走過的步數
    printf ("step %d: move %d from %c->%c\n", cnt, id, from, to);
}

void hanoi(int n, char x, char y, char z)
{
    if (n == 0)
        return;
    hanoi(n - 1, x, z, y);
    move(n, x, z);
    hanoi(n - 1, y, x, z);
}

int main()
{
    int n;
    printf("Please enter the number of the plates:");
    while (~scanf ("%d", &n) && n)
    {
        cnt = 0;
        printf ("The following are the steps for the question\n");
        hanoi(n, '1', '2', '3');
        printf ("There are %d steps in all.\nYou have solved the hanoi problems, congratulations!\n", cnt);
        printf ("Would you like to continue?(y/n)");
        char ch; scanf (" %c", &ch);
        if (ch == 'y' || ch == 'Y')
            printf("Please enter the number of the plates:\n");
        else
        {
            printf ("Here comes the end of the program. Bye\n");
            break;
        }
    }
    return 0;
}