汉诺塔问题Python代码---学习记录

一、前言

        作者在算法学习过程中,遇到了一些经典和令人难以理解的问题,坚持希望先依靠自己去解决问题的原则,自己收获很多兴奋的成果,即使很多早已在网上被研究过很多很多遍。因此想找个地方记录自己的学习过程。本篇则是利用递归函数解决的汉诺塔问题

n = 2

二、汉诺塔问题描述

        汉诺塔问题起源于一个古老的印度传说,简单描述如下:存在三根相同并排的柱子(起始柱,目标柱,辅助柱),第一根柱子上面有从小到大依次排列的n个盘子,大盘子在下面,小盘子在上面,现在需要将这n个盘子从起始柱移动到目标柱上。

        移动的规则是:①每次只能移动一个盘子,而且只能移动柱子上最顶部的圆盘;②大盘子不能放在小盘子上面。

汉诺塔问题

三、数学求解

        在这个问题里面我们需要找到一个将圆盘移动到目标柱的方法,更进一步的,还可以找到移动圆盘的最小步数。首先对这种毫无头绪的问题,不能从许多的圆盘开始尝试移动,我们需要从圆盘数量为1开始分析找规律。

1.求解数学规律

        (1)当 n = 1 时,显然将起始柱上的圆盘移动到目标柱即可。移动次数为

        n=1, MoveCount\left ( 1 \right )=1

n = 1

        (2)当 n = 2 时,则需要将起始柱上的小圆盘先移动到辅助柱上,再将起始柱上的大圆盘移动到目标柱上,最后辅助柱上的小圆盘移动到目柱上即可。

     n=2, MoveCount\left (2 \right )=3

      

n = 2

        (3)当 n = 3 时,我们先不进行移动操作,观察可知,第三个大圆盘移动受限最多,最不易被移动,主要是因为上面有两个较小的圆盘需要移动。因此我们可以将两个小圆盘看作一个合体圆盘,这样只需将合体圆盘移动到辅助柱上,大圆盘便可以移动目标柱,之后再把合体圆盘移到目标柱即可。

        合体圆盘的移动次数可以参照圆盘数为 2 时,移动次数 MoveCount2 有3次,而大圆盘的移动次数为1,则最终移动次数为

n=3,MoveCount\left (3 \right )=2\times MoveCount\left (2 \right )+1=2\times3+1=7

n = 3

        (4)此时我们可以考虑数量为 n 时,类比上述三个圆盘的移动方法,我们需要将 n - 1 个圆盘先放到辅助柱上,这样才可以移动第 n 个大圆盘从起始柱上移动到目标柱,接着再把 n - 1 个圆盘从辅助柱上移动到目标上,所以我们可以得到 MoveCount(n) = 2 ×   MoveCount(n - 1) + 1.

        为了便于公式书写,我们将 MoveCount(n) 写作 a_{n},这样我们便把移动次数的规律写成一个首项 a_{1} = 1数列 \left \{ a_{n} \right \} ,并得到递推公式

a_{n}=2 \times a_{n-1} + 1

        我们利用构造法来确定它的通项公式

a_{n}=2 \times a_{n-1} + 1 \Rightarrow a_{n}+1=2 \times a_{n-1} + 1+1=2 \times \left ( a_{n-1} + 1 \right )

a_{n}+1=2 \times \left(a_{n-1} +1\right )

        显然 \left \{ a_{n}+1 \right \} 是一个等比数列,我们可以得到

a_{n}+1=\left(a_{1}+1 \right ) \times 2^{n-1}=2 \times 2^{n-1}=2^{n}

\Rightarrow a_{n}=2^{n}-1

        至此,我们得到了一个移动圆盘的方法,并可以根据圆盘数量计算出移动次数。即我们要移动数量为 n 的圆盘时,我们需要先将 n - 1 个圆盘移动到辅助柱上(PS. 这里思考时比较费解,移动 n - 1 个圆盘时,先要把 n - 2 个圆盘移动到目标柱上,接着把第 n - 1 个圆盘移动到辅助柱上,然后再把 n - 2个圆盘移动到辅助柱上,这时我们将辅助柱作为目标柱,而目标柱作为辅助柱,其中递推时各柱子适时的身份转换是后面 Python 代码实现算法的关键。),再将第 n 个大圆盘移动到目标柱上,最后再把 n - 1 个圆盘从辅助柱上移动到目标柱上,而移动次数为 2^{n}-1 次。

2.移动最少次数

        接下来我们证明上述规律为移动的最少次数。当圆盘数量为 n = 1 和 n = 2 时,利用不重复移动的穷举法可以证明移动的最少次数。

        当圆盘数量为 n = 3 及以上时,我们可以注意到,第 n 个大圆盘是移动受限最多的圆盘,当它从起始柱移动到目标柱上时,其余 n - 1 个圆盘一定在辅助柱上。所以我们在移动第 n 个圆盘之前,必须先把 n - 1 个圆盘先移动到辅助柱上,而根据PS.所注,移动第 n - 1 个圆盘到辅助柱时,其余 n - 2 个圆盘一定先在目标柱上。依次类推到第一个圆盘后,可以发现每个圆盘的移动都是固定的,任何其他的移动方式都是非必须。因此,我们可以证明上述的移动方法可以得到最少的移动次数。

四、Python代码

1.递归函数伪代码

        作者在最初尝试解决这个问题时,并没有想到利用递归函数求解,也由于编程经验缺乏,无奈去编程帮网站(一个很好的编程学习网站)上看了伪代码之后才恍然大悟。惊叹于这个巧妙利用形参调换位置后,便可以打印输出所期望的移动方法。下面是来源于编程帮网站伪代码展示解决问题的过程 

hanoi(num , source , target , auxiliary):            // num 表示移动圆盘的数量,source、target、auxiliary 分别表示起始柱、目标柱和辅助柱
    if num == 1:                                                 // 如果圆盘数量仅有 1 个,则直接从起始柱移动到目标柱
        print(从 source 移动到 target)
    else:
        hanoi(num-1 , source , auxiliary , target) // 调用 hanoi 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
        print(从 source 移动到 target)                  // 将起始柱上剩余的最后一个大圆盘移动到目标柱上
        hanoi(n-1 , auxiliary , target , source)      // 调用 hanoi 函数,将辅助柱上的 num-1 圆盘移动到目标柱上

         作者认为这段递归函数伪代码很重要要的地方在于,用形参代指各个柱子,而且在内部调用过程中,通过输入不同的变量名,来模拟递推过程中各柱子身份的切换,如求解数学规律最后PS.所注那样,递推过程中,每个圆盘每步的移动是确定的,不过推理过程很复杂,因此直接交给计算机去进行推理。需要注意的是,在总圆盘数量改变时,每个圆盘每一步所移动的位置也需要发生改变。

        其实编写程序的过程中不需要进行非常复杂的思考,只需要将解决问题的逻辑用代码的形式展示出来,就能算是很成功的开头。如果解题的逻辑没有出现问题,那么报错大概率是语法出错。就如同上面的代码,其解题逻辑和本文前部分析得到方法别无二致,不过是用 if 判断巧妙设置了结束递归和进入递归的地方和打印输出所需结果。

2.Python代码实现

        作者根据提供的伪代码,编写出了解决汉诺塔问题的 Python 代码,并相较于网站提供的 Python 代码,添加了可以显示圆盘序号的功能,只需改变下面测试代码中函数的传入变量,即可输出相应数量的汉诺塔问题求解过程,代码如下

def hanoi(num):
    # 创建圆盘容器,用列表元素模拟取走或放上的圆盘
    source_disk = []
    auxiliary_disk = []
    target_disk = []

    for i in reversed(range(num)):      # 制造出一摞source上的初始圆盘
        source_disk.append(i + 1)

    # num 表示移动圆盘的数量,前三个列表参数,用于接受外层函数的圆盘容器,source、target、auxiliary 分别表示起始柱、目标柱和辅助柱
    def hanoi_way(num_in, source_list, target_list, auxiliary_list, source, target, auxiliary):
        '''
        设置六个参数,前三个列表表示各个圆盘移动后的情况,后三个元素代表柱子
        :return: 无
        '''
        if num_in == 1:  # 如果圆盘数量仅有 1 个,则直接从起始柱移动到目标柱
            print(f"将圆盘 {source_list[-1]} 从 {source} 移动到 {target}")
            target_list.append(source_list.pop())   # 此处需将移动放在打印之后,否则先移动会导致,列表中无元素而无法打印
        else:
            # 调用 hanoi 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
            hanoi_way(num_in - 1, source_list, auxiliary_list, target_list, source, auxiliary, target)
            print(f"将圆盘 {source_list[-1]} 从 {source} 移动到 {target}")  # 将起始柱上剩余的最后一个大圆盘移动到目标柱上
            target_list.append(source_list.pop())
            # 调用 hanoi 函数,将辅助柱上的 num-1 圆盘移动到目标柱上
            hanoi_way(num_in - 1, auxiliary_list, target_list, source_list, auxiliary, target, source)

    return hanoi_way(num, source_disk, auxiliary_disk, target_disk, "初始柱", "目标柱", "辅助柱")


if __name__ == '__main__':
    hanoi(4)

# 输出如下   
# 将圆盘 1 从 初始柱 移动到 辅助柱
# 将圆盘 2 从 初始柱 移动到 目标柱
# 将圆盘 1 从 辅助柱 移动到 目标柱
# 将圆盘 3 从 初始柱 移动到 辅助柱
# 将圆盘 1 从 目标柱 移动到 初始柱
# 将圆盘 2 从 目标柱 移动到 辅助柱
# 将圆盘 1 从 初始柱 移动到 辅助柱
# 将圆盘 4 从 初始柱 移动到 目标柱
# 将圆盘 1 从 辅助柱 移动到 目标柱
# 将圆盘 2 从 辅助柱 移动到 初始柱
# 将圆盘 1 从 目标柱 移动到 初始柱
# 将圆盘 3 从 辅助柱 移动到 目标柱
# 将圆盘 1 从 初始柱 移动到 辅助柱
# 将圆盘 2 从 初始柱 移动到 目标柱
# 将圆盘 1 从 辅助柱 移动到 目标柱


        为了添加新功能,作者首先是想到利用列表存储每个柱子当前状态上面的圆盘数,采用闭包函数,设置初始各个柱子上面的圆盘情况。然后也是依葫芦画瓢给内层函数又增加了三个列表形参,用作接受圆盘的容器。这样可以跟随打印输出的移动情况,相应的移动列表内的元素,并把每次移动的圆盘序号打印出来。

        需要注意的是,打印输出要在列表内元素移动前,否则会因为某时刻列表内没有元素而无法打印出报错。

IndexError:列表索引超出范围

3.代码实现 2

        作者后面又尝试用CSDN创作助手编写程序输出一下上述代码实现的功能,发现更加简洁,代码如下

def hanoi(n, from_tower, to_tower, aux_tower):
    """
    :param n: 盘子数量
    :param from_tower: 起始塔
    :param to_tower: 目标塔
    :param aux_tower: 辅助塔
    """
    if n == 1:
        print(f"Move disk 1 from {from_tower} to {to_tower}")
        return
    hanoi(n-1, from_tower, aux_tower, to_tower)
    print(f"Move disk {n} from {from_tower} to {to_tower}")
    hanoi(n-1, aux_tower, to_tower, from_tower)


# 测试
hanoi(3, 'A', 'C', 'B')
# ```
#
# 输出:
#
# ```
# Move disk 1 from A to C
# Move disk 2 from A to B
# Move disk 1 from C to B
# Move disk 3 from A to C
# Move disk 1 from B to A
# Move disk 2 from B to C
# Move disk 1 from A to C
# ```

        看到这里,作者发现自己学习的知识太少太少,这篇文章就算是自己学习记录的开头,争取以后都能把想法记录下来。

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值