C++抽象编程——递归策略(1)——汉诺塔问题详解(1)

来源:互联网 发布:数据安全研究员要求 编辑:程序博客网 时间:2024/05/19 06:49

汉诺塔(The Towers of Hanoi)

这个系列的第一个例子是一个简单的谜题,被称为汉诺塔(Towers of Hanoi)。19世纪80年代由法国数学家Edouard Lucas发明的Hanoi难题塔在欧洲迅速普及。它的成功部分原因是由于法国数学家Henri De Parville(由数学历史学家W. W. R. Ball翻译)在La Nature中描述的这个谜题古老的传说(为了保证传说的完整性,我就上英文版了):

In the great temple at Benares beneath the dome which marks the center of the world, rests a brass plate in which are fixed three diamond needles, each a cubit high and as thick as the body of a bee. On one of these needles, at the creation, God placed sixty-four disks of pure gold, the largest disk resting on the brass plate and the others getting smaller and smaller up to the top one.This is the Tower of Brahma. Day and night unceasingly, the priests transfer the disks from one diamond needle to another according to the fixed and immutable laws of Brahma, which require that the priest on duty must not move more than one disk at a time and that he must place this disk on a needle so that there is no smaller disk below it. When all the sixty-four disks shall have been thus transferred from the needle on which at the creation God placed them to one of the other needles, tower, temple and Brahmins alike will crumble into dust, and with a thunderclap the world will vanish.

大体的翻译一下:
在标记世界中心的圆顶下的贝纳雷斯的圣殿中,放置一个黄铜板,其中固定有三个钻石针,每个立方厘米的高度和厚度与蜜蜂的身体一样厚。 在这些其中的一根上,上帝创造了六十四块纯金的磁盘,其中最大的磁盘放在黄铜板的底部,其他的光盘越来越小,直到最上面。这是梵天塔(Tower of Brahma)。 昼夜不断地,祭司根据固定和不变的法律将磁盘从一个钻石针转移到另一个钻石针,这要求当值的牧师不要一次移动多个磁盘,并且他必须放置某个磁盘在针上,使它下面没有更小的磁盘。 当所有六十四个磁盘都应该从上帝把他们放在另一个针头上的针上转移时,塔,寺庙和婆罗门都会粉碎,并且随着雷霆,世界将消失。
多年来,这个地方已经从印度转移到了越南,但这个难题和传说依然存在。

实际上,Hanoi难题的塔没有实际用途,除了一个:用于教授计算机专业学生递归。 在这个领域,它具有巨大的价值,因为解决方案除了递归之外什么都不涉及。与真实世界问题的大多数递归算法相反,Hanoi问题没有任何可能干扰您的理解的无关复杂因素。因此有助于你了解递归解决方案的工作原理。因为它的工作原理如此之好以至于Hanoi塔被列入大多数教科书,用于递归的教学,在某个方面,地位相当于我们编程入门的“hello,world”程序。

现在,这个谜题已经作为了一个趣味的儿童益智游戏,商业版本中,64个金牌的传奇被八个木制或塑料片替代,这使得游戏更容易解决(更不用说更便宜)。所以,初始的疑题看起来是这样子的:

一开始,所有的盘子都在A里面,你要做的就是把A中的所有的盘子移动到B端,前提是:
- 一次只能移动一个盘子(You can only move one disk at a time
- 盘子的上方的另外一个盘子一定要比它小(You are not allowed to move a larger disk on top of a smaller disk

构成问题(Framing the problem)

为了在Hanoi问题上使用递归,我们首先必须在更一般的形式下构成这个问题。尽管最终目标是将八个磁盘从A移动到B,但问题的递归分解将涉及将较小盘子从尖顶移动到各种配置中的针上。在更一般的情况,我们需要解决的问题是将一个给定高度的塔从一个尖塔移动到另一个尖塔,使用第三个针C作为临时存储的中转站。为了确保所有子问题符合原始格式(我们的递归范式的要求),递归过程必须遵循以下参数:
1. 需要移动的盘子数(The number of disks to move
2. 盘子所在的初始针的名字(The name of the spire where the disks start out
3. 盘子所在的最终针的名字(The name of the spire where the disks should finish
4. 盘子所在的中转针的名字(The name of the spire used for temporary storage

要移动的磁盘数量显然是一个整数,并且针标有字母A,B和C。所以我们使用char类型来指示涉及哪跟针。知道这些类型我们就可以为移动盘子的操作编写一个原型,如下所示:

void moveTower(int n, char start, char finish, char tmp);

所以我们如果要移动8根,那么就可以这样调用函数:

moveTower(8, 'A', 'B', 'C');

此函数的调用对应的解释是“将盘子数为8的汉诺塔,从A移动到B,使用C作为中转”。随着递归分解的进行,moveTower将以不同的参数进行调用,这些参数将以各种配置移动较小的塔。

寻找递归策略(Finding a recursive strategy)

现在我们对问题有更一般的定义,可以回到找到移动大型塔的策略的问题了。记得我们在递归范式中讲到,要应用递归,我们必须首先确保该问题符合以下条件:
1. 这里肯定有一个simple case(There must be a simple case),在这个问题中,当n等于1时,会发生simple case,这意味着只有一个磁盘要移动。只要我们不违反在较小的磁盘上放置较大磁盘的规则,则可以单个操作移动单个磁盘。
2. 这里肯定有一个递归分解(There must be a recursive decomposition), 为了实现一个递归的解决方案,必须将问题分解成与原来相同的形式但是更加简单的问题。这部分问题更难,需要仔细检查。

要了解一个更简单的子问题如何有助于解决更大的问题,让我们回顾并考虑使用八个磁盘的原始示例:

这里的目标是将八个磁盘从A移到B.此时你需要问问自己,当盘子的数目更小的时候你是否能够解决相同的问题?特别是,你应该考虑如何移动一堆七个磁盘使得它可以帮助你解决移动八个磁盘的问题。
如果我们试想一下这个问题,很明显可以把它分成三个步骤来解决问题:
1. 将前面的7个盘子全部移动到C。(Move the entire stack consisting of the top seven disks from spire A to spire C.
2. 把底下的最大的盘子移动到B。(Move the bottom disk from spire A to spire B.
3. 将C中的盘子移动到B。(Move the stack of seven disks from spire C to spire B.

为了方便理解,我们用图片的形式来直观的表示这三个步骤:
第一步

第二步

第三步

没错!!就是这样,现在,你已经把盘子问题的规模从8个减少到了7个,接下来我们考虑的是第一步的7个盘子又可以通过相同的办法从A移动到B。从而我们推广到n个盘子的解决办法:
1. 从初始的A针,将N-1个盘子移动到中转站C。
2. 将剩下的一个盘子移动到终点针B。
3. 将中转站C的所有的盘子都移动到终点针B。

在这一点上,很难避免对自己说:“好吧,我可以减少移动N-1大小的塔的问题,但是该怎么做呢?”答案当然是以完全相同的方式你移动规模为N-1的盘子。你将这个问题分解成一个需要移动大小为N-2的塔,这进一步分解成移动N-3大小的塔,等等,直到只有一个磁盘移动。但从心理上讲,最重要的是避免完全提出这个问题。我们要有leap of faith观念。你已经在不改变其形式的前提下缩小了问题的规模。接下来艰苦的工作就留给计算机去操心吧(不然要它做什么)。

最后我们应用递归范式,写出对应的伪代码(下一篇博文会讲解怎么写它的代码跟分析):

void moveTower(int n, char start, char finish, char tmp) {    if (n == 1) {        Move a single disk from start to finish.    } else {        Move a tower of size n - 1 from start to tmp.        Move a single disk from start to finish.        Move a tower of size n - 1 from tmp to finish.    }}
0 0