汉诺塔问题的递归实现
汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
1.汉诺塔(基本)
汉诺塔问题是典型的分治算法问题,首先我们来讨论最基本的汉诺塔问题。假设有n个圆盘,三根柱子,a,b,c,需要把n个盘子(从下往上按照大小顺序摞着)从a柱移动到c柱,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
解决办法;
采用分治法,分而治之,把大问题化解成小问题。故可以把n个盘子看成n-1个盘子,和第n个盘子,首先我们需要把n-1个盘子移动到b柱子上,然后把第n个盘子移动到c柱子上,最后把n-1个盘子移动到c柱子上,这样就用最少的移动次数完成了任务。
c++实现:
- #include <iostream>
- using namespace std;
- void move(int n, char a, char b){
- cout<<a<<"->"<<b<<endl;
- }
- void hanoi(int n, char a, char b, char c){//把n个盘子从a柱子移动到b柱子
- if(n > 0) {
- hanoi(n - 1, a, c, b);// 把n-1个盘子移动到c柱子上
- move(n, a, b); // 把a移动到b
- hanoi(n - 1, c, b, a); // 把第n-1个盘子从c柱子移动到b柱子上
- }
- }
- int main()
- {
- int n;
- while(cin>>n){
- char a='a',b='b',c='c';
- hanoi(n,a,c,b); //把n个盘子从a柱子移动到c柱子
- }
- return 0;
- }
该程序显示全部步骤的移动方法。
计算移动次数:
如果要计算一共移动了多少次,找出规律即可。
假设移动n个盘子需要移动f(n)次,所以把n-1个盘子移动到b柱子上,需要移动f(n-1)次,然后把第n个盘子移动到c柱子上,需要移动1次,最后把n-1个盘子移动到c柱子上,需要移动f(n-1)次,综上所述,一共移动了
f(n) = 2 f(n-1) + 1
而:
f(1) = 1;
f(2) = 2*1+ 1;
f(3) = 2(2*1+ 1)+ 1;
.......
f(n) = 2^n -1
C++实现:
- //基本汉诺塔移动次数,可以算小于64的所有盘子数
- #include <iostream>
- using namespace std;
- int main()
- {
- int n;
- while(cin>>n){
- if(n>63||n<1) {
- cout<<"ERROR"<<endl;
- continue;
- }
- __int64 sum = 1;
- for(int i=1;i<=n;i++)
- sum*=2;
- cout<<sum-1<<endl;
- }
- return 0;
- }
2.汉诺塔(必须通过中间的柱子)
假设有n个圆盘,三根柱子,a,b,c,需要把n个盘子(上从下往上按照大小顺序摞着)从a柱移动到c柱,不允许直接从最左(右)边移到最右(左)边(每次移动一定是移到中间杆或从中间移出),也不允许大盘放到下盘的上面。
参考acm题目: http://acm.hdu.edu.cn/showproblem.php?pid=2064
这道题和之前不同,约束了必须通过中间的柱子才可以,这也难不倒我们,我们只需要在递归的时候改变一下策略就可以完成任务。
解决方法:
依然采用分治法,分而治之,把大问题化解成小问题。最开始在a柱子上有n个盘子,我们可以把n个盘子看成n-1个盘子,和第n个盘子。首先我们需要把n-1个盘子移动到c柱子上(n-1个盘子只能由a移动到c或者由c移动到a,否则就不是最少次数了),然后把第n个盘子移动到b柱子上(不能直接由a移动到c),最后把n-1个盘子移动到c柱子上,这样就用最少的移动次数完成了任务。
c++实现:
- #include <iostream>
- using namespace std;
- void move(int n, char a, char b){
- cout<<a<<"->"<<b<<endl;
- }
- void hanoi(int n, char a, char c, char b){
- if(n > 0) {
- hanoi(n - 1, a, c, b);
- move(n, a, b);
- hanoi(n - 1, c, a, b);
- move(n, b, c);
- hanoi(n - 1, a, c, b);
- }
- }
- int main()
- {
- char a='a',b='b',c='c';
- hanoi(3,a,c,b);
- return 0;
- }
计算移动次数:
如果要计算一共移动了多少次,找出规律即可。
假设移动n个盘子需要移动f(n)次,所以把n-1个盘子移动到c柱子上,需要移动f(n-1)次,然后把第n个盘子移动到b柱子上,需要移动1次,然后把n-1个盘子移动到a柱子上,需要移动f(n-1)次,第n个盘子移动到c柱子上,需要移动1次,最后把n-1个盘子移动到c柱子上,需要移动f(n-1)次,综上所述,一共移动了
f(n) = 3 f(n-1) + 2
而:
f(1) = 2;
f(2) = 3*2+ 2;
f(3) = 3(3*2+ 2)+ 2;
.......
f(n) = 3^n -1
C++实现:
- #include <iostream>
- using namespace std;
- int main()
- {
- int n;
- __int64 f[36];
- f[1]=2;
- for(int i=2;i<=35;i++) f[i]=3*f[i-1]+2;
- while(cin>>n){
- if(n>56||n<1){
- cout<<"ERROR!"<<endl;
- continue;
- }
- cout<<f[n]<<endl;
- }
- return 0;
- }
3汉诺塔(必须通过中间的柱子,允许最大的放在最上面)
假设有n个圆盘,三根柱子,a,b,c,需要把n个盘子(上从下往上按照大小顺序摞着)从a柱移动到c柱,不允许直接从最左(右)边移到最右(左)边(每次移动一定是移到中间杆或从中间移出),也不允许大盘放到小盘的上面。如果我们允许最大的盘子放到最上面会怎么样呢?(只允许最大的放在最上面)当然最后需要的结果是盘子从小到大排在最右边。
参考acm题目:http://acm.hdu.edu.cn/showproblem.php?pid=2077
这道题和之前不同,约束了必须通过中间的柱子才可以,也放宽了对最大盘子的限制,所以肯定比第二种汉诺塔移动次数少,我们也是只需要在递归的时候改变一下策略就可以完成任务。
解决方法:
依然采用分治法,分而治之,把大问题化解成小问题。最开始在a柱子上有n个盘子,我们可以把n个盘子看成n-2个盘子,和第n-1,以及第n个盘子。首先我们需要把n-2个盘子移动到c柱子上(利用第二种类型的算法),然后把第n-1和n个盘子依次移动到b柱子上,再把n-2个盘子移动到a柱子上,再把第n和第n-1个盘子移动到柱子c上,这样就用最少的移动次数完成了任务。
C++实现:
- #include <iostream>
- using namespace std;
- void move(int n, char a, char b){
- cout<<a<<"->"<<b<<endl;
- }
- void hanoi(int n, char a, char c, char b){
- if(n > 0) {
- hanoi(n - 1, a, c, b);
- move(n, a, b);
- hanoi(n - 1, c, a, b);
- move(n, b, c);
- hanoi(n - 1, a, c, b);
- }
- }
- void Hanoi_(int n, char a, char c, char b){
- if(n > 0) {
- hanoi(n - 2, a, c, b);
- if(n > 1)
- move(n - 1, a, b);
- move(n, a, b);
- hanoi(n - 2, c, a, b);
- move(n, b, c);
- if(n > 1)
- move(n - 1, b, c);
- hanoi(n - 2, a, c, b);
- }
- }
- int main()
- {
- int n;
- while(cin>>n) {
- char a='a',b='b',c='c';
- Hanoi_(n,a,c,b);
- }
- return 0;
- }
计算移动次数:
如果要计算一共移动了多少次,找出规律即可。
假设移动n个盘子需要移动F(n)次,用第二种算法把n-2个盘子移动到c柱子上(利用第二种类型的算法),需要f(n-2)次,然后把第n-1和n个盘子依次移动到b柱子上,需要2次,再把n-2个盘子移动到c柱子上,再把第n和第n-1个盘子移动到柱子c上,需要2次,综上所述,一共移动了
F(n) = 3 f(n-2) + 4
而:f(n-2) = 3^(n-2)-1,故F(n)=3^(n-1)+1
C++实现;
- #include <iostream>
- using namespace std;
- int main()
- {
- int T;
- cin>>T;
- while(T--){
- int n;
- cin>>n;
- if(n>51||n<1) {
- cout<<"ERROR"<<endl;
- continue;
- }
- __int64 sum = 1;
- for(int i=1;i<=n-1;i++)
- sum*=3;
- cout<<1+sum<<endl;
- }
- return 0;
- }
4.汉诺塔(加一根柱子)
假设有n个圆盘,三根柱子,a,b,c,需要把n个盘子(上从下往上按照大小顺序摞着)从a柱移动到c柱,再找来了一根一模一样的柱子d,通过这个柱子来更快的把所有的盘子移到第三个柱子上。
参考acm题目:http://acm.hdu.edu.cn/showproblem.php?pid=1207
这道题和之前都有很大的不同,加了一根柱子,意味着有的时候可用3根柱子,有的时候可用4根柱子,当把j个小盘子移动到d盘上时,有四根柱子可用,而当把n-j个盘子从a移动到c时,仅有三根柱子可用。这里我们就要找到j的值,使所有移动的次数和最小。
解决方法:
依然采用分治法。首先把j个盘子移动到d柱子上(通过四个柱子可用的算法),需要B[j]次移动,然后把n-j个盘子移动到c柱子上(通过三个柱子可用的算法),需要A[n-j]次移动,,然后把d中的j个盘子移动到c柱子上,需要B[j]次移动。我们可以用j的大小循环,找到移动次数最小的j。
首先我们先计算移动的次数:
核心公式为:flag=2*B[j]+A[i-j]; //j个盘子移动到第四个柱子,然后把剩下的i-j个在第四个不能用的情况下移到第三个
计算次数的c++实现:
- #include <iostream>
- using namespace std;
- int main()
- {
- int n;
- double a[65],b[65]; //数组a代表没加第四个柱子的结果,数组b代表加了第四个柱子的结果
- a[1]=1;
- for(int i=2;i<=64;i++) a[i]=2*a[i-1]+1;
- b[1]=1;
- b[2]=3;
- for(int i=3;i<=64;i++){
- double min=a[i],flag;
- for(int j=1;j<i;j++){
- flag=2*b[j]+a[i-j]; //j个移动到第四个柱子,然后把剩下的i-j个在第四个不能用的情况下移到第三个柱子上
- if(min>flag) min=flag;
- }
- b[i]=min;
- }
- while(cin>>n){
- if(n>64||n<1){
- cout<<"ERROR!"<<endl;
- continue;
- }
- cout<<b[n]<<endl;
- }
- return 0;
- }
此时利用上面的算法,我们就可以模拟出移动的步骤了,
模拟移动步骤的C++实现:
- #include <iostream>
- using namespace std;
- void move(int n, char a, char b){
- cout<<a<<"->"<<b<<endl;
- }
- void hanoi_basic_3(int n, char a, char b, char c){//把n个盘子从a柱子移动到b柱子
- if(n > 0) {
- hanoi_basic_3(n - 1, a, c, b);// 把n-1个盘子移动到b柱子上
- move(n, a, b); // 把a移动到b
- hanoi_basic_3(n - 1, c, b, a); // 把第n个盘子移动到b柱子上
- }
- }
- void hanoi(int n, char a, char c, char b, char d, int C[]){
- int j=C[n]; //j个盘子使用四个柱子的移动方式
- if(n > 0) {
- hanoi(j, a, d, b, c, C);// 把j个盘子移动到d柱子上
- hanoi_basic_3(n - j, a, c, b);// 把n-j个盘子移动到c柱子上(使用三个柱子的移动方式)
- hanoi(j, d, c, a, b, C); // 把j个盘子移动到c柱子上
- }
- }
- int main()
- {
- int n,flag_j;
- double A[65]; //未加第四个柱子时候的移动次数情况
- A[1]=1;
- for(int i=2;i<65;i++) A[i]=2*A[i-1]+1;
- double B[65],flag,min;
- int C[65]; //把前 c[j]个元素用四个柱子的方法,后i-j 个元素用三个柱子的方法
- C[1]=0;
- C[2]=0;
- for(int i=3;i<=64;i++){
- min=A[i]; //数组A代表没加第四个柱子的结果次数,数组b代表加了第四个柱子的结果次数
- B[1]=1;
- B[2]=3;
- flag_j=0;
- for(int j=1;j<i;j++){
- flag=2*B[j]+A[i-j]; //j个移动到第四个柱子,然后把剩下的i-j个在第四个不能用的情况下移到第三个
- if(min>flag) {
- min=flag;
- flag_j=j;
- }
- B[i]=min;
- C[i]=flag_j;
- }
- }
- while(cin>>n){
- char a='a',b='b',c='c',d='d';
- hanoi(n,a,c,b,d,C); //把n个盘子从a柱子移动到c柱子
- }
- return 0;
- }