递归解决汉诺塔问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/TuringGo/article/details/82698202
            </div>
                                                <!--一个博主专栏付费入口-->
         
         <!--一个博主专栏付费入口结束-->
        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-4a3473df85.css">
                                    <div id="content_views" class="markdown_views">
                <!-- flowchart 箭头图标 勿删 -->
                <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
                    <path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path>
                </svg>
                                        <h1 id="前言"><a name="t0"></a>前言</h1>

先来看两个有趣的故事

  • 从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙。。。
    • 问:GNU全称是什么?
    • 答:GNU is Not UNIX
    • 问:第一个单词GNU的全称是什么?
    • 答:GNU is Not UNIX
    • 问:第一个单词GNU的全称是什么?
    • 答:GNU is Not UNIX
    • 。。。

类似于上述这种使用自己来定义自己的结构,称之为递归

递归思想很有意思,在很多方面都有实际的用途。在计算机编程中,对于一些特定的问题,常规方法往往会显得很繁琐,抓不住问题的本质,这种时候采用递归的思想编程,往往能够写出很优雅的代码。

汉诺塔问题

“汉诺塔”是由数学家爱德华 · 卢卡斯于1883年发明的游戏,爱德华当初用了一个神话来描述这个问题

传说越南河内某间寺院有三根银棒,上串 64 个金盘。寺院里的僧侣依照一个古老的预言,以上述规则移动这些盘子;预言说当这些盘子移动完毕,世界就会灭亡。这个传说叫做梵天寺之塔问题(Tower of Brahma puzzle)。

—— 维基百科

这个问题被数学抽象描述就是

  • 问题:有3根细柱子(A、B、C),其中A柱子上有64个圆盘,64个圆盘按照从上到下由小到大的次序排列。求一共需要移动多少次才能将A柱子上的64个圆盘全部移动到B柱子上?
  • 移动规则(移动前、移动中、移动后,都必须遵循)
    • 一次只能移动柱子最上端的一个圆盘
    • 小圆盘上面不能放大圆盘

对于64个圆盘问题,太复杂太庞大了。可以先从最小的情况开始考虑

一层汉诺塔

算法图解如下

一层汉诺塔图解

规律:

  • 将1个圆盘,从A柱子移动到B柱子

记作:ABA→B

两层汉诺塔

算法图解如下

两层汉诺塔图解

规律:

  • 将2-1个圆盘,从A柱子移动到C柱子
  • 将第2个圆盘,从A柱子移动到B柱子
  • 将2-1个圆盘,从C柱子移动到B柱子

记作:

  • ACA→C
三层汉诺塔

算法图解如下

三层汉诺塔图解01

三层汉诺塔图解02

规律

  • 将3-1个圆盘,从A柱子移动到C柱子(第1次~第3次)
  • 将第3个圆盘,从A柱子移动到B柱子(第4次)
  • 将3-1个圆盘,从C柱子移动到B柱子(第5次~第7次)

记作:

  • ABACBCA→B,A→C,B→C
规律一

由于第四层汉诺塔整个算法图解过程已经很长了,所以这里就不列举了。不过,从前面三层汉诺塔图解算法中,我们足以发现了n层汉诺塔移动的一般性规律

  • 将n-1个圆盘,从A柱子移动到C柱子
  • 将第n个圆盘,从A柱子移动到B柱子
  • 将n-1个圆盘,从C柱子移动到B柱子

我们将上述三个步骤,调换一下顺序

  • 将n-1个圆盘,从A柱子移动到C柱子
  • 将n-1个圆盘,从C柱子移动到B柱子
  • 将第n个圆盘,从A柱子移动到B柱子

调换顺序之后的发现,其实第一步和第二步合起来,便是将n-1个圆盘从A柱子移动到B柱子上

因此,算法可以进一步简化为

  • 将n-1个圆盘,从A柱子移动到B柱子
  • 将第n个圆盘,从A柱子移动到B柱子

将n-1个圆盘从A移动到B的过程,就是将n-2个圆盘从A移动到B,与将第n-1个圆盘从A移动到B;将n-2个圆盘从A移动到B的过程,就是将n-3个圆盘从A移动到B,与将第n-2个圆盘从A移动到B;……;将1个圆盘从A移动到B的过程,就是将第1-1个圆盘从A移动到B,与将第1个圆盘从A移动到B。

很明显,这样的一个过程,就是递归。在整个递归过程中,无论从第几层开始,最后会终止于一个确定的步骤——将1个圆盘从A移动到B,而每一层最关键的一个确定步骤就是——将第n个圆盘从A移动到B。因此整个递归过程其实都是在做一件事情——将1个圆盘从A移动到B

规律二

由规律一,我们可以得知,整个递归过程中都是在重复做一件事情——将1个圆盘从A移动到B。但事实上,一旦圆盘数目超过1个时,就需要一个借助3根柱子中的一根作为中转站,通过折中的方法实现将圆盘从A的移动到B。

  • 将n-1个圆盘,从A柱子移动到C柱子
  • 将第n个圆盘,从A柱子移动到B柱子
  • 将n-1个圆盘,从C柱子移动到B柱子

由上述总结规律可知,这样的一个过程,起到中转站的作用的柱子似乎就是C柱子。

但是事实上绝不是这样,很多情况下,起到中转站作用的柱子不是绝对的!

分析一个最具代表的模型,三层汉诺塔的表达式

  • ABACBCA→B,A→C,B→C

从整个算法过程来看,以B为中转站能够将2个盘子从A移动到C,以A为中转站能够将2个盘子从C移动到B。在更多层的汉诺塔算法过程中,往往是A、B、C都会对某一段过程起到中转站的作用,因此使用中转站的概念来抽象出基本表达式是难以把握的。

因此,这里建议采用了一种目标柱子的概念(没听过对吧,我自创的)来理解上述的过程。

  • 首先,将算法中最特殊的一个步骤——从A移动到B,看做本次移动的目标柱子是B,因此在这样一个步骤中, 右边出现最多的就是B。

OK!到此,我们可以得知,在整个算法中,只有两个目标柱子——B和C存在。

算法实现

通过上面的一系列分析,我们得出了两个最为关键性的结论。根据这两个相关性结论,编写代码实现算法

C语言实现算法

#include <stdio.h>

//在参数x、y、z中,永远以中间那个参数为“目标柱子”
void hanoi(int n, char x, char y, char z)
{
    if (n == 1) {
        //递归的最后一种情况,1层汉诺塔:从A移动到B
        printf("第%d层圆盘移动方向 : %c -> %c\n", n, x, y);
    } else {
        //第一步,以C柱子为目标(规律二)
        hanoi(n - 1, x, z, y);

        //第二步,以B柱子为目标,直接输出(规律一)
        printf("第%d层圆盘移动方向 : %c -> %c\n", n, x, y);

        //第三步,以B柱子为目标(规律二)
        hanoi(n - 1, z, y, x);
    }
}

int main(void)
{
    hanoi(1, 'A', 'B', 'C');
    putchar('\n');
    hanoi(2, 'A', 'B', 'C');
    putchar('\n');
    hanoi(3, 'A', 'B', 'C');
    putchar('\n');
    hanoi(4, 'A', 'B', 'C');

    return 0;
}

/* 运行结果:
第1层圆盘移动 : A -> B

第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B


第1层圆盘移动 : A -> B
第2层圆盘移动 : A -> C
第1层圆盘移动 : B -> C
第3层圆盘移动 : A -> B
第1层圆盘移动 : C -> A
第2层圆盘移动 : C -> B
第1层圆盘移动 : A -> B


第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
第3层圆盘移动 : A -> C
第1层圆盘移动 : B -> A
第2层圆盘移动 : B -> C
第1层圆盘移动 : A -> C
第4层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
第2层圆盘移动 : C -> A
第1层圆盘移动 : B -> A
第3层圆盘移动 : C -> B
第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
*/
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

如分析中那样,递归算法采用的终止条件是——1层汉诺塔情况,但实际上这个算法是缺乏健壮性不合理的,而且存在冗余。因为它没能考虑汉诺塔最特殊的情况——0层汉诺塔 。当程序运行hanoi(0)时,函数无法返回,造成异常错误。事实上,0层汉诺塔是存在且合理的,而且把0层汉诺塔考虑进去才符合我们对汉诺塔问题的本质分析——每一次都是从A移动到B ,而且我们可以发现上述代码中1层汉诺塔也并没有什么特殊之处,函数中两个printf()语句执行的操作是一模一样的。下面给出改进版本的代码

Python实现算法

def hanoi(n, x, y, z):
    if not n:
        pass   #0层汉诺塔不需要做任何移动
    else:
        hanoi(n-1, x, z, y)
        print("第%d层圆盘移动方向:%c -> %c" % (n, x, y))
        hanoi(n-1, z, y, x)

hanoi(0, 'A', 'B', 'C')
# 什么也不输出

hanoi(1, 'A', 'B', 'C')
第1层圆盘移动方向:A -> B

hanoi(2, 'A', 'B', 'C')
第1层圆盘移动方向:A -> C
第2层圆盘移动方向:A -> B
第1层圆盘移动方向:C -> B

hanoi(3, 'A', 'B', 'C')
第1层圆盘移动方向:A -> B
第2层圆盘移动方向:A -> C
第1层圆盘移动方向:B -> C
第3层圆盘移动方向:A -> B
第1层圆盘移动方向:C -> A
第2层圆盘移动方向:C -> B
第1层圆盘移动方向:A -> B
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

改进版本的Python代码,不但能正确计算出汉诺塔问题,而且算法还能够兼容最特殊的情况——0层汉诺塔问题。这才是该问题最优雅的算法。

问题答案

回到汉诺塔的问题上,64个圆盘的汉诺塔需要移动多少次才能完成从A柱子到B柱子。

通过上面发现的规律,以及算法的实现,我们可以得出该算法的递归公式

H(n)=H(n1)+1+H(n1)H(n)=H(n−1)+1+H(n−1)

使用递推公式计算汉诺塔移动次数

H(0)=0H(0)=0

H(1)=H(0)+1+H(0)=0+1+0=1H(1)=H(0)+1+H(0)=0+1+0=1

H(2)=H(1)+1+H(1)=1+1+1=3H(2)=H(1)+1+H(1)=1+1+1=3

H(3)=H(2)+1+H(2)=3+1+3=7H(3)=H(2)+1+H(2)=3+1+3=7

H(4)=H(3)+1+H(3)=7+1+7=15H(4)=H(3)+1+H(3)=7+1+7=15

H(5)=H(4)+1+H(4)=15+1+15=31H(5)=H(4)+1+H(4)=15+1+15=31

H(6)=H(5)+1+H(5)=31+1+31=63H(6)=H(5)+1+H(5)=31+1+31=63

......

观察函数与函数值的规律可得知

H(0)=0=201H(0)=0=20−1

H(1)=1=211H(1)=1=21−1

H(2)=3=221H(2)=3=22−1

H(3)=7=231H(3)=7=23−1

H(4)=15=241H(4)=15=24−1

H(5)=31=251H(5)=31=25−1

H(6)=63=261H(6)=63=26−1

......

因此,通项表达式为

H(n)=2n1H(n)=2n−1

n = 64 时,H(64) = 18446744073709551615。就算是每秒可以移动一次,也需要5845亿年多,而目前所知的宇宙的年龄也不过才137亿年,所以说就算传说当真,那么距离世界末日还早着呢。。。

总结

递归算法的关键就在于抓住递归中重复的步骤,然后每次递归都是在重复那一关键步骤。递归算法虽然内存开销比较大,但是代码可读性很好,而且体现出了一种数学的优雅美(反正我觉得很美!)。

数学中分形,具有递归美,下面放出福利图片,送给大家欣赏(图片来源:http://www.matrix67.com,侵权必删!)

科赫雪花

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值