19 12
发新话题
打印

[原创] 直接写出移盘步骤的非递归非栈的汉诺塔算法

本主题由 System 于 2008-1-1 05:00 解除限时精华

直接写出移盘步骤的非递归非栈的汉诺塔算法

大概在初中的时候,玩起了汉诺塔小游戏,曾经还乐此不疲。慢慢的总结出了一些规律。无论几个盘子,都能很快的以最少步搬完。后来学C语言,看到了谭浩强那本书上关于汉诺的观点,心里有点疑惑。再后来张小东老师给我们上操作系统,一次在课后向张老师谈起了汉诺塔,说到曾经的一点想法。得到张老师的鼓励,于是有了半个月废寝忘食的投入。得出了两种非递规非栈算法。写完了,有点不力量力,整理成了小论文,本来有点想法。后来一直封存在电脑里。想想还是发到这里,分享一下想法,或者可以当作一种参考。如何在必要的时候抛弃用栈去转化递规算法。。
   内容有点长,代码我发部分上来,两种解法我称之为:树解汉诺塔算法和图解汉诺塔算法。其实本身和树与图没什么太大得联系,只是描述起来有点类似。方便起见我这样称呼。

    直接写出移盘步骤的非递归非栈的汉诺塔算法
               ――――一种可以在PC机上移动任意多个盘子的汉诺塔算法
                             
                         指导教师:哈工大(威海)计算机科学与技术学院   张小东
                                          哈工大(威海)数学系04级: 王逸群
                              
一:概要
关于汉诺塔的移盘算法,常规的做法是用递归来做。或者是用将递归用栈转化的解决一般递归问题的非递归算法来做。这样的算法有个共性:耗内存。并且占用的内存随盘子数量N的增大呈指数级数增长。面对30个盘子的移动步骤,1G内存心有余而力不足。
对汉诺塔移动规律进行深入的分析,我发现汉诺塔的规律是有迹可循的。继而我导出了两种不用栈的的非递归算法,一个是从树形规律发祥的,不妨称为:树解汉诺塔算法;二是从图形规律发现的,不妨称为:图解汉诺塔。这两种算法都不用递归来做,对N个盘子的移动,消耗的内存在3N到4N之间。和2的N次方指数幂相比,优化是很可观的。
二:移动规律
下面将给出两种算法的基本思路,在此之前,先将两个算法的理论依据即规律在此说明,首先约定几个规则:
一:首先将汉诺塔的三根柱子标记为:柱S(盘子初始位置),柱T(临时存放盘子的堆),柱D(盘子搬运的目的地);
二:将S,T,D三柱按D,S,T摆放(这样做不会影响移动),并将它们循环看待,即从D到S为向右移,T到D也看为向右移动。同理,S到D和D到T都看为是向左移;
三:将初始盘子从低(最大盘子)到顶(最小盘子)按1到N标上序号,并规定在移动过程中各盘子的序号总是不变的。
有了上面的三个规则,下面给出汉诺塔移动规律:
   A:在移动过程中,任一步骤,必满足奇偶规则,这个奇偶规则是:移动的盘子若是奇数,则必向左移,若是偶数,则必向右移动(注意:这里的左右是有上面三个约定前提而定的)
   B:这个规律稍复杂一些,首先,对于N个盘子,移动所需最小步骤为2的N次方减一,
这个由数学归纳法很明显证明。将N个盘子的移动步骤分为N大步,第一大步为将序号为1盘子移至D,则必先
a:将上面N-1个盘子移至T,由上面的最小步骤数需要2^(N-1)-1步(2^X表示2的X次方)步;
b:将1号盘子从S移至D,为一步
上述两步相加:2^(N-1)-1+1=2^(N-1)
依此类推,对于第M大步,它是将序号为N-M的盘子移至D堆(注意:此时未必是从S到D)显然它需要的步骤数为2^(N-M-1),因为要先将上面的N-M-1个盘子移到另一柱,
则最小步数为2^(N-M-1)-1,最后将N-M移至D,共2^(N-M-1)步。
第N大步为将N号盘子移动到D,显然是2^(N-N)=1步。
第M大步涉及到移动的盘子显然是N-M号盘到N号盘共M个盘,将每大步内的2^(N-M-1)步称为小步,都从1到2^(N-M-1)表示,那么小步骤small_step和移动的盘子序号满足下面关系(将移动时要移动的盘子称为操作数(data_op)),移动N-J号盘子时(M<N-J<N),其步骤数必满足:2^(J+1)*K+2^J(0=<K,且(2^(J+1)*K+2^J)<=2^(N-J)),则只要满足(STEP-2^J) MOD 2^(J+1) =0 的步骤STEP,该步骤移动的盘子的序号必是N-J。
三:两种解法思路
A,B两个移动规律我将在后面给出证明,这里先给出两种算法的思路:
1:树解汉诺塔算法
     分析指明,若将小步骤数按大步序号分层排列,并将每个小步看为一个节点,把大步看为层数,则整个移动步骤数按上述规定构成一个高为N的完全二叉树。(确切的说是一颗倒立的二叉树),按正常二叉树看待,节点满足,所有右节点为N,所有左节点为N减去右节点。如图:





从该图就很明显看出汉诺塔的递规规律乐,每一大步可分为两部分,前一部分为下一步的重复,后一部分仅将最后一小步的盘子序号减小1。若将结点写成两子结点的差,则上图变为:



对与N个盘子分为N大步搬,这里就要用N个循环for(Big_step=1;Bg_step<=N;Big_step++)
在每个大步骤Big_step里一小步如此移动:
1:首先根据步骤STEP找出该步要移动的盘子序号,这个盘子序号的取得由规律B中算出,从P到N扫描,若满足(STEP-2^J) MOD 2^(J+1) =0,则该步骤移动的盘子为H+1-J(事实上由于P本身不可能达到N,这点由上面B的K范围可以看出,同样由上面,P一定能在它本身的范围找到,所以这里不用在设置它的范围,只要取个最大的数,保证循环能把所有的情况取完就可以了,而且比较次数的平均值是能算出来的。),下面给出该函数的C++代码,这里用2^P做为循环,因为整形数据用LOG函数的话会损失精度,这里用两个变量跟踪数据变化,可以避免每次计算pow(2,P)的值,从而缩短运行时间.
int Get_data_op(int step)//取得操作数函数
{
        if(step%2==1)
                return H;//奇数位步骤为H
        else{
int         Max_pow_H=int(pow(2,H));

                int m2=2;int e2=2;
                while(e2<=Max_pow_H)
                        {
                                if ((step-e2)%(e2*2)==0)
                                        return (H+1-m2);
                                m2++;
                                e2*=2;
                        }
        }return 0;
}
(这样做一定不会出现对本不是该类的步骤却返回了同一个盘子序号的情况,见证明A_1)
2:接下来这步就比较简单了,根据int Get_data_op(int step)返回的盘子序号寻找该盘子所在柱,由于只能移动每个柱子的顶端的盘子,所以用三各循环,依次比较三个柱子顶元素即可。
3:寻找移入的柱子,这个也简单,由于该步移动的盘子序号已经知道,那么可以知道它的奇偶性,再者又由上面2知道了移出的柱子,根据规律A就可以知道移入柱子了。
上面三步就可以移动盘子了,下面给出这个解法的从C++源代码:
分析:该算法由步骤数STEP给出该步骤移动盘子序号,当盘子过大时由于数字太大,可以解决大数运算的方法解决,这已不是这里关心的问题。但这样会影响算法的执行效率。
而图解汉诺塔法则要好得很多。



证明A_1:
  设有两个步骤STEP1STEP2,设STEP1=2^(I+1)×K2^ISTEP2=2^(J+1)*K+2^Ji!=j
(这里上面两式要满足上面得条件)
1):第一种情况是i>j
          这时 2^(i+1)*k+2^i2^j/(2^(j+1)=2(ij)*k+2^(ij1)1/2
a
容易知道(a)必不为整数;
2):第二种情况时i<j


这时(2^(i+1)*k+2^i2^j/(2^(j+1)2^(ij)*k+2^(ij1)1/2
              b=ji(b>=1)则有2^(b)*k+2^(b1)1/2=k/(2^(b))+1/(2^(b+1))1/2
(b)

从(b)式看1/(2^(b+1))1/2=(2^b1)/(2^(b+1))分母为一个2^(b+1)项而前项k/(2^(b))不可能分解为一个整数和一个分母为2^(b+1)数量级的和。那么上式必不可能分解为一个整数式。
由上面两种情况知道,按减后作模对自然数的等价类划分是可行的,也就是说按上面的做法,不可能会出现下面两种错误:
1:原本对应小模的差,没有在作小模的时候找到对应的操作数,而错误的对应了后面的大模对应操作数;
2:原本应作大模差对应的操作数,在错误的在作小模就找到了一个错误的操作数。
   
  
树解汉诺塔的c++代码:
#include<iostream.h>
#include<stdio.h>
#include<conio.h>
#include<io.h>
#include<math.h>
#include<string.h>
#include<stdlib.h>
FILE *fptr;//用于将移动步骤保存到文本文件里
#define MAXSIZE_H 100000000//移动盘子数量
int string[MAXSIZE_H];//用来输出时的暂存的
typedef struct data_home{

int body[MAXSIZE_H];


int ta_h;


char data_flag;

}data_home;//用来存储每个柱子的盘子
data_home *s;
data_home *d;
data_home *t;//三个柱子的盘子
data_home *now_data;//指向移入的柱子
data_home *last_data;//指向移出的柱子
int now_data_f=0;//表示当前柱子,与data_home->data_flag对应
int H;//表示塔的高度


int Get_data_op(int step);//取得操作数函数
void Move_data_op();//移动盘子函数
data_home *Get_data_home_out(int data_op);//取得移出柱子
data_home *Get_data_home_in(int now_data_f);//取得移入柱子
void Print_data_home();//输出函数
//////////////////////////////////////////////////
void main()
{


s=new(data_home);


s->data_flag ='s';



t=new(data_home);


t->data_flag ='t';




d=new(data_home);


d->data_flag ='d';//
申请三个柱子空间,这里三个柱子都申请了H个空间


now_data=s;


last_data=s;


while(1)//
选择菜单,便于调试数据

{


char ifstart;


cout<<"
s开始移塔"<<endl;

cout<<"
q退出"<<endl;


ifstart=getch();


if(ifstart=='q')


exit(0);


else if(ifstart=='s')


{
Move_data_op();//
移盘函数

}


}





}
                                                                      2007年5月



代码实在太长,发上来后我又删除了绝大部分,有人感兴趣的话我再发。奇偶规律的证明和图解汉诺算法我暂作保留。如果必要我再考虑发上来。

         在这里也要感谢张小东老师那时耐心的听我条理不清的证明论述!


(再者,代码中大部分的头文件是没必要的,编程时习惯了把各种常用的头文件加上。初始时给每个“塔”放真正的“盘子”其实也时没有必要的,而且因此看不到结果(我运行了半个多小时,大概才放上2000000个盘子)去了之后,可以直接看到结果,还有是如果盘子数过大,可能提示数组过大,但一定程度内还是可以运行的,只要把初始赋值去了,只出每次结果就可以)

           电邮:yiqunwang1985@yahoo.com.cn
          QQ 405441695
这个是程序运行的结果图,为了显示明了,我用了很多回车,所以这里只能显示一部分。见谅

上图也可以看出,每步都真正的为塔上放上了盘子,这样很方便的分析结果,但是运行速度大大降低,去了后就快多了。
   感谢子小吉吉耐耐心心的看完我冗长的证明,并指出其中的错误。其中
移动N-J号盘子时(M<N-J<N),其步骤数必满足:2^(J+1)*K+2^J(0=<K,且(2^(J+1)*K+2^J)<=2^(N-J)),则只要满足(STEP-2^J) MOD 2^(J+1) =0 的步骤STEP,该步骤移动的盘子的序号必是N-J。
的做法后边也提到了
在每个大步骤Big_step里一小步如此移动:
1:首先根据步骤STEP找出该步要移动的盘子序号,这个盘子序号的取得由规律B中算出,从P到N扫描,若满足(STEP-2^J) MOD 2^(J+1) =0,则该步骤移动的盘子为H+1-J(事实上由于P本身不可能达到N,这点由上面B的K范围可以看出,同样由上面,P一定能在它本身的范围找到,所以这里不用在设置它的范围,只要取个最大的数,保证循环能把所有的情况取完就可以了,而且比较次数的平均值是能算出来的。)
我把各各盘号对应的步骤做一张表格就明了了:


                       首步骤到0的距离          对应盘号                         步骤数
                                    1                          N                                   1 3 5 7 9。。。
                                    2                         N-1                                 2 6 10 14 18。。。。
                                    4                         N-2                                 4 12 20 28 。。。
                                    8                         N-3                                 8 24 30 46。。。。
这样,等价类的关系和上面的证明就很明了了!




[ 本帖最后由 yiyanwan77 于 2007-11-6 09:45 编辑 ]
附件: 您所在的用户组无法下载或查看附件

TOP

呵呵,厉害,应该鼓励这样的帖子

不过这也真正体现出了数学和计算机学生的差异,也就是理科和工科思维的差异.

理科在于找到方法,这里给出的是汉塔问题的求解方法;侧重点是在人为的找出解决问题的规律和方法.可以看成是人求解,计算机实现.

曾经我也是这一类型的,不过学计算机学的好的,慢慢的都会转换思路.

而工科方法是尽可能的避开人为因素,尽力减轻人的负担或者思考,用抽象概念去探索解决问题. 于是有了递归等算法,此类问题一个算法搞定,但是换一个类汉塔问题,理科的方法就不一定能用了.

不过这两种思维是不可分割的,相互促进,同样重要与有用,就是不同的侧重.

[ 本帖最后由 浮生 于 2007-10-22 16:11 编辑 ]
本帖最近评分记录
  • 刹那芳华 威望 +2 ....评论的有理 2007-10-25 11:54

TOP

我是工科的,喜欢递归。。。。。。
非递归?学习一下吧。。。。
他大舅他二舅都是他舅,高桌子低板凳都是木头~~

TOP

A(n) = 2*A(n-1) + 1

TOP

精品:例子
我在等待着什么......

TOP

回复 2# 的帖子

很中肯的回复,谢谢!
我说抛弃递规做法的话的确有点绝对了,一般的类汉诺塔问题,递规简单明了。
但是对于某个大规模的问题,比如需要很大的空间(时间),而无法用时间(空间)
换取时,就要寻找别的方法来做了.
谭浩强上说,任何人都无法写出汉诺塔移动每步的步骤。可是正如上面已经给出的证明和算法
只要保证所做的移动是最小步的,就一定能根据步骤写出该步的移动盘号和方向。可以证明,移动的情况是唯一的。

TOP

回复 4# 的帖子

正是如此,每步的盘号和方向由等价类划分得到。

TOP

回复 5# 的帖子

谢谢!

TOP

回复 3# 的帖子

一起讨论~~

TOP

引用:
原帖由 yiyanwan77 于 2007-10-25 21:47 发表
正是如此,每步的盘号和方向由等价类划分得到。
高中我玩同学的电子词典上的诺亚舟的时候,我就考虑这个问题,只要给出盘子的数目,就能给出最少移动步数


前几天看了你的帖子,想了想
给出了盘子的数目,要求移动步数最少,
可以用数组来记录每个盘子的状态 ,用三个变量记录柱子上的状态,然后根据一定的算法,就能自动地给出移动的步骤。

不过之前要分析在最少移动步数的 情况下 每个盘子移动的规律 ,每个盘子移动的规律和它的序号有关

你的帖子太长了,没细看,不知道是不是这个意思



能用等价类分析,楼主从知到行,佩服

[ 本帖最后由 飘如陌上尘 于 2007-10-25 22:26 编辑 ]

TOP

 19 12
发新话题