赞
踩
作者在算法学习过程中,遇到了一些经典和令人难以理解的问题,坚持希望先依靠自己去解决问题的原则,自己收获很多兴奋的成果,即使很多早已在网上被研究过很多很多遍。因此想找个地方记录自己的学习过程。本篇则是利用递归函数解决的汉诺塔问题
汉诺塔问题起源于一个古老的印度传说,简单描述如下:存在三根相同并排的柱子(起始柱,目标柱,辅助柱),第一根柱子上面有从小到大依次排列的n个盘子,大盘子在下面,小盘子在上面,现在需要将这n个盘子从起始柱移动到目标柱上。
移动的规则是:①每次只能移动一个盘子,而且只能移动柱子上最顶部的圆盘;②大盘子不能放在小盘子上面。
在这个问题里面我们需要找到一个将圆盘移动到目标柱的方法,更进一步的,还可以找到移动圆盘的最小步数。首先对这种毫无头绪的问题,不能从许多的圆盘开始尝试移动,我们需要从圆盘数量为1开始分析找规律。
(1)当 n = 1 时,显然将起始柱上的圆盘移动到目标柱即可。移动次数为
(2)当 n = 2 时,则需要将起始柱上的小圆盘先移动到辅助柱上,再将起始柱上的大圆盘移动到目标柱上,最后辅助柱上的小圆盘移动到目柱上即可。
(3)当 n = 3 时,我们先不进行移动操作,观察可知,第三个大圆盘移动受限最多,最不易被移动,主要是因为上面有两个较小的圆盘需要移动。因此我们可以将两个小圆盘看作一个合体圆盘,这样只需将合体圆盘移动到辅助柱上,大圆盘便可以移动目标柱,之后再把合体圆盘移到目标柱即可。
合体圆盘的移动次数可以参照圆盘数为 2 时,移动次数 MoveCount2 有3次,而大圆盘的移动次数为1,则最终移动次数为
(4)此时我们可以考虑数量为 n 时,类比上述三个圆盘的移动方法,我们需要将 n - 1 个圆盘先放到辅助柱上,这样才可以移动第 n 个大圆盘从起始柱上移动到目标柱,接着再把 n - 1 个圆盘从辅助柱上移动到目标上,所以我们可以得到 MoveCount(n) = 2 × MoveCount(n - 1) + 1.
为了便于公式书写,我们将 MoveCount(n) 写作 ,这样我们便把移动次数的规律写成一个首项 = 1数列 ,并得到递推公式
我们利用构造法来确定它的通项公式
即
显然 是一个等比数列,我们可以得到
至此,我们得到了一个移动圆盘的方法,并可以根据圆盘数量计算出移动次数。即我们要移动数量为 n 的圆盘时,我们需要先将 n - 1 个圆盘移动到辅助柱上(PS. 这里思考时比较费解,移动 n - 1 个圆盘时,先要把 n - 2 个圆盘移动到目标柱上,接着把第 n - 1 个圆盘移动到辅助柱上,然后再把 n - 2个圆盘移动到辅助柱上,这时我们将辅助柱作为目标柱,而目标柱作为辅助柱,其中递推时各柱子适时的身份转换是后面 Python 代码实现算法的关键。),再将第 n 个大圆盘移动到目标柱上,最后再把 n - 1 个圆盘从辅助柱上移动到目标柱上,而移动次数为 次。
接下来我们证明上述规律为移动的最少次数。当圆盘数量为 n = 1 和 n = 2 时,利用不重复移动的穷举法可以证明移动的最少次数。
当圆盘数量为 n = 3 及以上时,我们可以注意到,第 n 个大圆盘是移动受限最多的圆盘,当它从起始柱移动到目标柱上时,其余 n - 1 个圆盘一定在辅助柱上。所以我们在移动第 n 个圆盘之前,必须先把 n - 1 个圆盘先移动到辅助柱上,而根据PS.所注,移动第 n - 1 个圆盘到辅助柱时,其余 n - 2 个圆盘一定先在目标柱上。依次类推到第一个圆盘后,可以发现每个圆盘的移动都是固定的,任何其他的移动方式都是非必须。因此,我们可以证明上述的移动方法可以得到最少的移动次数。
作者在最初尝试解决这个问题时,并没有想到利用递归函数求解,也由于编程经验缺乏,无奈去编程帮网站(一个很好的编程学习网站)上看了伪代码之后才恍然大悟。惊叹于这个巧妙利用形参调换位置后,便可以打印输出所期望的移动方法。下面是来源于编程帮网站伪代码展示解决问题的过程
- 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 判断巧妙设置了结束递归和进入递归的地方和打印输出所需结果。
作者根据提供的伪代码,编写出了解决汉诺塔问题的 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 从 辅助柱 移动到 目标柱
-
-
为了添加新功能,作者首先是想到利用列表存储每个柱子当前状态上面的圆盘数,采用闭包函数,设置初始各个柱子上面的圆盘情况。然后也是依葫芦画瓢给内层函数又增加了三个列表形参,用作接受圆盘的容器。这样可以跟随打印输出的移动情况,相应的移动列表内的元素,并把每次移动的圆盘序号打印出来。
需要注意的是,打印输出要在列表内元素移动前,否则会因为某时刻列表内没有元素而无法打印出报错。
作者后面又尝试用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
- # ```
看到这里,作者发现自己学习的知识太少太少,这篇文章就算是自己学习记录的开头,争取以后都能把想法记录下来。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。