1. 程式人生 > >漢諾塔(必須經過中間柱子)遞迴與非遞迴詳解與程式碼實現

漢諾塔(必須經過中間柱子)遞迴與非遞迴詳解與程式碼實現

首先介紹一下漢諾塔最初始的規則:

有三根相鄰的柱子,標號為A,B,C,A柱子從上到下按照金字塔狀疊放著n個不同大小的圓盤,現在把所有的盤子一個一個移動到柱子B上,並且每次移動同一根柱子上都不能出現大盤子在小盤子上方。

這是最初始的規則,實現的思路可以分為兩個步驟:
(假設圓盤期初都在左邊的柱子上,想移動到右邊的柱子上)
1.如果只有一個圓盤,直接把左邊的圓盤移動到右邊。
2.如果有n個圓盤(n>1),先把1—n–1圓盤移動到中間的柱子上,最後一個圓盤進行第一個步驟,之後再把1—n-1圓盤從中間移動到右邊。
給出一個博文的連結:開啟連結,該博主寫得簡單易懂,十分推薦。

升級規則:不能將圓盤直接從左邊移動到右邊,必須經過中間的柱子

遞迴法:

其實思路與升級前的遞迴是一樣的:
1.當只有一個圓盤時,先從左移到中間,再從中間移動到右邊。
2.當有兩個圓盤時,先把1(最上面的圓盤)從左邊移到中間,再把1移動到右邊。把2(1圓盤下面的圓盤)從左邊移動到中間,然後把1從右邊移動到中間,再把1移動到左邊,把2從中間移動到右邊,把1從左邊移動到中間,再移動到右邊。
3.……
總結來說我們要做的有兩個步驟:
1、當只要一個圓盤時:
1)如果起點或者終點中有一個為中間柱子,直接把圓盤從起始柱子移動到目標柱子。
2)如果起點和終點都不為中間的柱子,則需要兩步,首先把圓盤從起始柱子移動到中間柱子,在從中間柱子移動到目標柱子。
2、當有n(n>1)個圓盤時,需要把1—n-1圓盤從左邊移動到右邊,再將最底下的圓盤n移動到中間,把1—n-1圓盤從右邊移動到左邊,把圓盤n從中間移動到右邊,1—n-1圓盤從左邊移動到右邊。
程式碼:

//hannuota II  遞迴
#include<iostream>
#include<vector>
#include<string>

using namespace std;

/*變數含義
* n:圓盤數量
*left:左柱子
*mid:中間柱子
*right:右柱子
*from:起點柱子
*to:目標柱子
*/
long hannuota(int n,string left,string mid,string right,string from,string to)
{
    if(n == 1)
    {
        if(from == mid||to == mid)
        {
            cout
<<"將"<<n<<"從"<<from<<"移動到"<<to<<endl; return 1; } else { cout<<"將"<<n<<"從"<<from<<"移動到"<<mid<<endl; cout<<"將"<<n<<"從"<<mid<<"移動到"<<to<<endl; return 2; } } else { long num1 = hannuota(n-1,left,mid,right,from,to); cout<<"將"<<n<<"從"<<from<<"移動到"<<mid<<endl; long num2 = hannuota(n-1,left,mid,right,to,from); cout<<"將"<<n<<"從"<<mid<<"移動到"<<to<<endl; long num3 = hannuota(n-1,left,mid,right,from,to); return num1+num2+num3+2; } } int main() { int n; cout<<"盤數:"; cin>>n; string from = "left"; string depend = "mid"; string to = "right"; if(n>=1) { cout<<"步數"<<hannuota(n,from,depend,to,from,to)<<endl; } return 0; }

測試:
盤數為1
盤數為2
當然,上述的程式碼只能實現將n個圓盤從左邊全部移到右邊,或者右邊移到左邊,如果起點或者終點為中間圓盤,即會出錯。但是實現全部只需要再加一種情況就好了。

(全面考慮)如果起點或者終點中有一個為中間柱子:

只剩一個圓盤時,直接移動到目標柱子,上面的程式碼已經包含,
不止一個圓盤時,需要把1—n-1圓盤移動到過渡柱子上(既不是起始柱子,也不是終點柱子),然後把圓盤n從起點移到終點,在把1—n-1從過渡移到終點。
更改 hannuota()函式

long hannuota(int n,string left,string mid,string right,string from,string to)
{
    if(n == 1)
    {
        if(from == mid||to == mid)
        {
            cout<<"將"<<n<<"從"<<from<<"移動到"<<to<<endl;
            return 1;
        }
        else
        {
            cout<<"將"<<n<<"從"<<from<<"移動到"<<mid<<endl;
            cout<<"將"<<n<<"從"<<mid<<"移動到"<<to<<endl;
            return 2;
        }
    }
    else
    {
        if(from == mid||to == mid)
        {
            string depend = (from == left||to == left)?right:left;
            long num1 = hannuota(n-1,left,mid,right,from,depend);
            cout<<"將"<<n<<"從"<<from<<"移動到"<<to<<endl;
            long num2 = hannuota(n-1,left,mid,right,depend,to);
            return num1+num2+1;
        }
        else
        {
            long num1 = hannuota(n-1,left,mid,right,from,to);
            cout<<"將"<<n<<"從"<<from<<"移動到"<<mid<<endl;
            long num2 = hannuota(n-1,left,mid,right,to,from);
            cout<<"將"<<n<<"從"<<mid<<"移動到"<<to<<endl;
            long num3 = hannuota(n-1,left,mid,right,from,to);
            return num1+num2+num3+2;
        }
    }
}

至此,遞迴方法已經說明

非遞迴:

藉助棧實現,構建3個棧,分別代表3個柱子。整個漢諾塔的步驟其實只有4步,從左移到中間,從中間移到左邊,從中間移到右邊,從右邊移到中間。而且這四步中滿足兩個條件:
1、相鄰不可逆:意思就是我執行了“從中間移到左邊”,下一步就不行執行“從左邊移到中間”,不然之前的步驟就沒有意義了,求出來的也不是最短的步數。
2、 小壓大原則:壓入的圓盤必須比柱子上最上面的圓盤小,否則不行。

而且每次選擇時,可以根據這兩個條件,排除4步中其他3步,只有1個滿足兩個條件,因此就選擇那種方法移動。證明:
假設上一步是“從左到中間”,則根據相鄰不可逆排除“從中間到左“,而且根據小壓大,“從中間到 右”和“從右到中間”只有一個符合,因此下一步是可以根據這兩個條件來唯一確定的。

程式碼實現:

//hanbuota II 非遞迴
#include<iostream>
#include<vector>
#include<string>
#include<stack>

using namespace std;

enum Action{
    NO,LTOM,MTOL,RTOM,MTOR
};

int fstackTotstack(Action &a,Action preAction,Action nowAction,stack<int>&fstack,stack<int >&tstack,string from,string to)
{
    if(preAction != a&&fstack.size()!=0&&(tstack.size()==0||fstack.top()<tstack.top()))
    {
        tstack.push(fstack.top());
        cout<<"將"<<fstack.top()<<"從"<<from<<"移動到"<<to<<endl;
        fstack.pop();
        a = nowAction;
        return 1;
    }
    return 0;
}


int hannuota(int n,string left,string mid,string right)
{
    stack<int>fstack;
    stack<int>mstack;
    stack<int>tstack;
    for(int i = n;i>=1;i--)
        fstack.push(i);
    int step = 0;
    Action a = NO;
    while(tstack.size()!=n)
    {
        step += fstackTotstack(a,MTOL,LTOM,fstack,mstack,left,mid);
        step += fstackTotstack(a,LTOM,MTOL,mstack,fstack,mid,left);
        step += fstackTotstack(a,RTOM,MTOR,mstack,tstack,mid,right);
        step += fstackTotstack(a,MTOR,RTOM,tstack,mstack,right,mid);

    }
    return step;
}


int main()
{
    int n;
    string left = "left";
    string mid = "mid";
    string right = "right";
    while(cin>>n)
    {
        if(n>=1)
            cout<<"步數"<<hannuota(n,left,mid,right)<<endl;
    }
    return 0;
}

測試:
漢諾塔_非遞迴

注意:當起始圓盤不在左邊柱子上時,需要更改四個fstackTotstack()函式的順序。