汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着若干片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。计算若干片金片全部移动到另外一个针上时需要移动的最少步数。
解题思路
要把n片圆盘从a柱子移动到c柱子上,第一步唯一的选择是移动a最上面的那个圆盘,但是应该将其移到b还是c呢?很难确定。因为接下来的第二步、第三步……直到最后一步,看起来都是很难确定的。能立即确定的是最后一步:最后一步的盘子肯定也是a最上面那个圆盘,并且是由a或b移动到c——此前已经将n-1个圆盘移动到了c上。
分解问题
先假设除最下面的盘子之外,我们已经成功地将上面的n-1个盘子移到了b柱,此时只要将最下面的盘子由a移动到c即可。如图:
当最大的盘子由a移到c后,b上是余下的n-1个盘子,a为空。因此现在的目标就变成了将这n-1个盘子由b移到c。这个问题和原来的问题完全一样,只是由a柱换为了b柱,规模由n变为了n-1。因此可以采用相同的方法,先将上面的62个盘子由b移到a,再将最下面的盘子移到c……过程为:
将b柱子作为辅助,把a上的63个圆盘移动到b上
将a上最后一个圆盘移动到c
将a作为辅助,把b上的62个圆盘移动到a上
将b上的最后一个圆盘移动到c
…
规律是每次都是先将其他圆盘移动到辅助柱子上,并将最底下的圆盘移到c柱子上,然后再把原先的柱子作为辅助柱子,并重复此过程。移动步数的规律为:每增加一个盘子,它的移动步数就增加原来步数的一倍加1。如:我们已经知道5个盘子移动31步,那么,6盘子就是31*2+1=63步。
因此使用教材(注:本人使用的教材为《计算机常用算法与程序设计案例教程(第二版)》,主编为杨克昌)上递归的方式解决该问题,其递归关系为:g(n)=2g(n-1)+1,初始条件(递归出口)为:g(1)=1。(P106)
教材上的解法(P106—P107):
假设函数hanoimove(m, x, y, z)用于将n个圆盘由x移动到z,y作为辅助柱子。用下面的代码实现这个移动过程:
void hanoimove(int m,char x,char y,char z) //显示汉诺塔移动的过程
{
if(m==1) mv(x,z);
else
{
hanoimove(m-1,x,z,y); //将x柱子上m-1个盘子借助z柱子,移动到y柱子
mv(x,z); //再直接将c柱子上的最后一个盘子移动到x
hanoimove(m-1,y,x,z); //然后将y柱子上的m-1个盘子借助x移动到z
}
}
用下面的代码实现移动次数的计数:
double hanoinum (int m)
{
double num=0;
if(m==1)
num=1;
else
num=2* hanoinum(m-1)+1;
return num;
}
如果将这个问题的盘子数量减为56个或更少,就不会出现的问题。但盘子数量为57个乃至更多的话,所走的步数是一个天文数字。由于使用整型和浮点型变量,内存限制,会存在溢出的风险,如下图
输出结果可以看出,输出步数的后三位变为0,与真实步数有差异。
教材上解决的对策是当输出金片大于40时,控制输出格式为“%0.4e”,即使用科学计算法解决溢出的问题,但是此方法会导致输出的数据不准确,如下图
因此,尝试使用字符串类型解决溢出问题。
程序实现
金片移动步数的另一种规律用公式表达为:2^m-1,其中m为金片数量。这个思路提供了另一种用C语言解决汉诺塔问题的办法,完整代码如下:
#include <stdio.h>
int main()
{
int m, i, cf, fi, t; //m用来保存汉诺塔金片数量,i用来控制循环,cf控制进位,fi用来判断第一位,t用来临时保存i值
char a[100000];
a[0] = '1'; //初始化指为1
a[1] = '\0';
printf("请输入金片数量:");
scanf("%d", &m);
while(m--)
{
i = 0; cf = 0; fi = 0;
while(a[++i]); //获得当前数组的存储内容最大下标。以便控制循环和赋结尾符
t = i; //保存i值
if(a[0]>'4')
fi = 1;
else
fi = 0;
while(i--)
{
if(a[i]>'4')
{
a[i+fi] = (a[i]-'0') * 2 % 10 + cf + '0';
cf = 1;
}
else
{
a[i+fi] = (a[i]-'0') * 2 + cf + '0';
cf = 0;
}
}
a[t+fi] = '\0';
if(fi) a[0] = '1';
}
i = 0;
while(a[++i]);
a[i-1] = a[i-1]-'0'-1 + '0'; //公式2^n-1中的‘-1’操作
printf("共需要%s步\n",a);
return 0;
}
运行结果:
通过运行结果可以看出,用字符串变量可以解决数据溢出的问题。