漢諾塔(必須經過中間柱子)遞迴與非遞迴詳解與程式碼實現
首先介紹一下漢諾塔最初始的規則:
有三根相鄰的柱子,標號為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;
}
測試:
當然,上述的程式碼只能實現將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()函式的順序。