当前位置:   article > 正文

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

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.递归函数伪代码

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

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

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

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

2.Python代码实现

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

  1. def hanoi(num):
  2. # 创建圆盘容器,用列表元素模拟取走或放上的圆盘
  3. source_disk = []
  4. auxiliary_disk = []
  5. target_disk = []
  6. for i in reversed(range(num)): # 制造出一摞source上的初始圆盘
  7. source_disk.append(i + 1)
  8. # num 表示移动圆盘的数量,前三个列表参数,用于接受外层函数的圆盘容器,source、target、auxiliary 分别表示起始柱、目标柱和辅助柱
  9. def hanoi_way(num_in, source_list, target_list, auxiliary_list, source, target, auxiliary):
  10. '''
  11. 设置六个参数,前三个列表表示各个圆盘移动后的情况,后三个元素代表柱子
  12. :return: 无
  13. '''
  14. if num_in == 1: # 如果圆盘数量仅有 1 个,则直接从起始柱移动到目标柱
  15. print(f"将圆盘 {source_list[-1]}{source} 移动到 {target}")
  16. target_list.append(source_list.pop()) # 此处需将移动放在打印之后,否则先移动会导致,列表中无元素而无法打印
  17. else:
  18. # 调用 hanoi 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
  19. hanoi_way(num_in - 1, source_list, auxiliary_list, target_list, source, auxiliary, target)
  20. print(f"将圆盘 {source_list[-1]}{source} 移动到 {target}") # 将起始柱上剩余的最后一个大圆盘移动到目标柱上
  21. target_list.append(source_list.pop())
  22. # 调用 hanoi 函数,将辅助柱上的 num-1 圆盘移动到目标柱上
  23. hanoi_way(num_in - 1, auxiliary_list, target_list, source_list, auxiliary, target, source)
  24. return hanoi_way(num, source_disk, auxiliary_disk, target_disk, "初始柱", "目标柱", "辅助柱")
  25. if __name__ == '__main__':
  26. hanoi(4)
  27. # 输出如下
  28. # 将圆盘 1 从 初始柱 移动到 辅助柱
  29. # 将圆盘 2 从 初始柱 移动到 目标柱
  30. # 将圆盘 1 从 辅助柱 移动到 目标柱
  31. # 将圆盘 3 从 初始柱 移动到 辅助柱
  32. # 将圆盘 1 从 目标柱 移动到 初始柱
  33. # 将圆盘 2 从 目标柱 移动到 辅助柱
  34. # 将圆盘 1 从 初始柱 移动到 辅助柱
  35. # 将圆盘 4 从 初始柱 移动到 目标柱
  36. # 将圆盘 1 从 辅助柱 移动到 目标柱
  37. # 将圆盘 2 从 辅助柱 移动到 初始柱
  38. # 将圆盘 1 从 目标柱 移动到 初始柱
  39. # 将圆盘 3 从 辅助柱 移动到 目标柱
  40. # 将圆盘 1 从 初始柱 移动到 辅助柱
  41. # 将圆盘 2 从 初始柱 移动到 目标柱
  42. # 将圆盘 1 从 辅助柱 移动到 目标柱

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

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

IndexError:列表索引超出范围

3.代码实现 2

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

  1. def hanoi(n, from_tower, to_tower, aux_tower):
  2. """
  3. :param n: 盘子数量
  4. :param from_tower: 起始塔
  5. :param to_tower: 目标塔
  6. :param aux_tower: 辅助塔
  7. """
  8. if n == 1:
  9. print(f"Move disk 1 from {from_tower} to {to_tower}")
  10. return
  11. hanoi(n-1, from_tower, aux_tower, to_tower)
  12. print(f"Move disk {n} from {from_tower} to {to_tower}")
  13. hanoi(n-1, aux_tower, to_tower, from_tower)
  14. # 测试
  15. hanoi(3, 'A', 'C', 'B')
  16. # ```
  17. #
  18. # 输出:
  19. #
  20. # ```
  21. # Move disk 1 from A to C
  22. # Move disk 2 from A to B
  23. # Move disk 1 from C to B
  24. # Move disk 3 from A to C
  25. # Move disk 1 from B to A
  26. # Move disk 2 from B to C
  27. # Move disk 1 from A to C
  28. # ```

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

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号