分治——汉诺塔问题

一、介绍

汉诺塔问题源自一种古老的传说故事。在古印度的一个寺庙里,僧侣们有三根高大的钻石柱
子,以及64个大小不一的金圆盘。僧侣们不断地移动原盘,他们相信在最后一个圆盘被正确
放置的那一刻,这个世界就会结束。然而,即使僧侣们每秒钟移动一次,总共需要大约 ≈1.84×秒,合约5850亿年,远远超过了现在对宇宙年龄的估计。(原因参考文末)所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。

在归并排序和构建二叉树中,我们都是将原问题分解为两个规模为原问题一半的子问题。然而对于汉诺塔问 题,我们采用不同的分解策略。

给定三根柱子,记为A、B和C。起始状态下,柱子A上套着𝑛个圆盘,它们从上到下按照从 小到大的顺序排列。我们的任务是要把这𝑛个圆盘移到柱子C上,并保持它们的原有顺序不 变。在移动圆盘的过程中,需要遵守以下规则。

1. 圆盘只能从一个柱子顶部拿出,从另一个柱子顶部放入。

2. 每次只能移动一个圆盘。

3. 小圆盘必须时刻位于大圆盘之上。

我们将规模为𝑖的汉诺塔问题记做𝑓(𝑖)。例如𝑓(3)代表将3个圆盘从A移动至C的汉诺塔问题。

1. 考虑基本情况

如下图所示,对于问题𝑓(1),即当只有一个圆盘时,我们将它直接从A移动至C即可。

如下图所示,对于问题𝑓(2),即当有两个圆盘时,由于要时刻满足小圆盘在大圆盘之上,因此需要借助 B来完成移动。

1. 先将上面的小圆盘从A移至B。

2. 再将大圆盘从A移至C。

3. 最后将小圆盘从B移至C。

解决问题𝑓(2)的过程可总结为:将两个圆盘借助B从A移至C。其中,C称为目标柱、B称为缓冲柱。

2. 子问题分解

对于问题𝑓(3),即当有三个圆盘时,情况变得稍微复杂了一些。

因为已知𝑓(1)和𝑓(2)的解,所以我们可从分治角度思考,将A顶部的两个圆盘看做一个整体,执行下图所示的步骤。这样三个圆盘就被顺利地从A移动至C了。

1. 令B为目标柱、C为缓冲柱,将两个圆盘从A移动至B。

2. 将A中剩余的一个圆盘从A直接移动至C。

3. 令C为目标柱、A为缓冲柱,将两个圆盘从B移动至C。

本质上看,我们将问题𝑓(3)划分为两个子问题𝑓(2)和子问题𝑓(1)。按顺序解决这三个子问题之后,原问题随之得到解决。这说明子问题是独立的,而且解是可以合并的。

至此,我们可总结出下图所示的汉诺塔问题的分治策略:

将原问题𝑓(𝑛)划分为两个子问题𝑓(𝑛−1) 和一个子问题𝑓(1),并按照以下顺序解决这三个子问题。

1. 将𝑛−1个圆盘借助C从A移至B。
2. 将剩余1个圆盘从A直接移至C。
3. 将𝑛−1个圆盘借助A从B移至C。

对于这两个子问题𝑓(𝑛−1),可以通过相同的方式进行递归划分,直至达到最小子问题𝑓(1)。而𝑓(1)的解是已知的,只需一次移动操作即可。

二、代码实现

在代码中,我们声明一个递归函数dfs(i, src, buf, tar) ,它的作用是将柱src 顶部的𝑖个圆盘借助缓冲柱buf移动至目标柱tar 。

def move(src: list[int], tar: list[int]):
    """移动一个圆盘"""
    # 从 src 顶部拿出一个圆盘
    pan = src.pop()
    # 将圆盘放入 tar 顶部
    tar.append(pan)


def dfs(i: int, src: list[int], buf: list[int], tar: list[int]):
    """求解汉诺塔问题 f(i)"""
    # 若 src 只剩下一个圆盘,则直接将其移到 tar
    if i == 1:
        move(src, tar)
        return
    # 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf
    dfs(i - 1, src, tar, buf)
    # 子问题 f(1) :将 src 剩余一个圆盘移到 tar
    move(src, tar)
    # 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar
    dfs(i - 1, buf, src, tar)


def solve_hanota(A: list[int], B: list[int], C: list[int]):
    """求解汉诺塔问题"""
    n = len(A)
    # 将 A 顶部 n 个圆盘借助 B 移到 C
    dfs(n, A, B, C)


"""Driver Code"""
if __name__ == "__main__":
    # 列表尾部是柱子顶部
    A = [5, 4, 3, 2, 1]
    B = []
    C = []
    print("初始状态下:")
    print(f"A = {A}")
    print(f"B = {B}")
    print(f"C = {C}")

    solve_hanota(A, B, C)

    print("圆盘移动完成后:")
    print(f"A = {A}")
    print(f"B = {B}")
    print(f"C = {C}")

汉诺塔问题形成一个高度为𝑛的递归树,每个节点代表一个子问题、对应一个开启的dfs() 函数,因此时间复杂度为𝑂(),空间复杂度为𝑂(𝑛)

汉诺塔问题的递归分解结构(f(n) = 2f(n-1) + 1)源于其规则限制与递归特性,具体原因如下:

  1. 问题分解的逻辑
    要移动n个圆盘,必须先将‌最上层的n-1个圆盘‌从起始柱移动到中转柱(对应第一个f(n-1)),腾出位置后才能将‌最底层的第n个圆盘‌直接移动到目标柱(对应f(1) = 1步)。最后再将中转柱上的n-1个圆盘移动到目标柱(第二个f(n-1)。这种“三明治”结构(n-1→1→n-1)是唯一满足“每次只能移动一个盘”且“大盘不可压小盘”规则的合法操作序列‌。

  2. 递归的数学表达
    递推公式f(n) = 2f(n-1) + 1直接体现了这一分叉结构:‌2f(n-1)‌:两次移动n-1个圆盘的步骤(从起始柱到中转柱、再从中转柱到目标柱)‌+1‌:移动最大圆盘的单次操作‌。例如,f(3) = 2f(2) + 1 = 2*(2f(1)+1)+1 = 7步,与手动模拟结果一致‌。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿梭的编织者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值