[Python] 借助 Turtle 来展示汉诺塔

背景

turtle --- Turtle graphics 中提供了关于 turtle\text{turtle} turtle 模块的介绍。我想到可以用 turtle\text{turtle} turtle 来展示汉诺塔 ( Tower of Hanoi\text{Tower of Hanoi} Tower of Hanoi) 的移动过程。于是写了本文。本文有两个主要部分

  • 提供展示汉诺塔操作的完整 Python\text{Python} Python 代码
  • 提供汉诺塔最小移动次数的分析

正文

完整的代码

我让 trae 帮我生成了相关的代码(我做了些调整),完整的代码如下 (为了简化互动过程,代码中只涉及4个盘子)

python 复制代码
import turtle

# ===================== 游戏配置 =====================
NUM_DISKS = 4
TOWER_COUNT = 3

SCREEN_WIDTH = 700
SCREEN_HEIGHT = 500

TOWER_HEIGHT = 180
TOWER_WIDTH = 8
DISK_HEIGHT = 25
DISK_WIDTH_BASE = 50

TOWER_X = [-200, 0, 200]
TOWER_Y = -150

# ===================== 游戏数据 =====================
towers = [[4, 3, 2, 1], [], []]
selected = None
win = False

# ===================== 初始化屏幕和画笔 =====================
screen = turtle.Screen()
screen.setup(SCREEN_WIDTH, SCREEN_HEIGHT)
screen.title("汉诺塔 - Tower of Hanoi")
screen.tracer(0)

pen = turtle.Turtle()
pen.hideturtle()

# ===================== 绘制函数 =====================


def draw_tower(x, y, height):
    pen.penup()
    pen.goto(x - TOWER_WIDTH // 2, y)
    pen.pendown()
    pen.fillcolor("gray")
    pen.begin_fill()
    pen.goto(x + TOWER_WIDTH // 2, y)
    pen.goto(x + TOWER_WIDTH // 2, y + height)
    pen.goto(x - TOWER_WIDTH // 2, y + height)
    pen.goto(x - TOWER_WIDTH // 2, y)
    pen.end_fill()


def draw_disk(x, y, disk_size, is_selected=False):
    width = DISK_WIDTH_BASE + disk_size * 25
    pen.penup()
    pen.goto(x - width // 2, y)
    pen.pendown()

    if is_selected:
        pen.fillcolor("yellow")
    else:
        pen.fillcolor("orange" if disk_size % 2 == 0 else "skyblue")

    pen.begin_fill()
    pen.goto(x + width // 2, y)
    pen.goto(x + width // 2, y + DISK_HEIGHT)
    pen.goto(x - width // 2, y + DISK_HEIGHT)
    pen.goto(x - width // 2, y)
    pen.end_fill()

    pen.pencolor("gray")
    pen.pensize(2)
    pen.goto(x + width // 2, y)
    pen.goto(x + width // 2, y + DISK_HEIGHT)
    pen.goto(x - width // 2, y + DISK_HEIGHT)
    pen.goto(x - width // 2, y)
    pen.pensize(1)
    pen.pencolor("black")


def draw_text(msg, y_offset=150):
    pen.penup()
    pen.goto(0, y_offset)
    pen.pendown()
    pen.write(msg, align="center", font=("Arial", 18, "normal"))


def redraw():
    pen.clear()

    for i in range(TOWER_COUNT):
        draw_tower(TOWER_X[i], TOWER_Y, TOWER_HEIGHT)

    for tower_idx in range(TOWER_COUNT):
        tower = towers[tower_idx]
        x = TOWER_X[tower_idx]
        for disk_idx, disk_size in enumerate(tower):
            y = TOWER_Y + disk_idx * DISK_HEIGHT
            is_selected = (selected == tower_idx)
            draw_disk(x, y, disk_size, is_selected)

    if win:
        draw_text("🎉 You Win! 🎉")
    elif selected is not None:
        draw_text(
            f"Selected Tower {selected + 1} - Click target tower to move"
        )
    else:
        draw_text("Click a tower to pick a disk, then click target tower")

    screen.update()


# ===================== 游戏逻辑 =====================


def move_disk(from_tower, to_tower):
    if not towers[from_tower]:
        return False
    if not towers[to_tower] or towers[from_tower][-1] < towers[to_tower][-1]:
        towers[to_tower].append(towers[from_tower].pop())
        return True
    return False


def check_win():
    return len(towers[2]) == NUM_DISKS


# ===================== 点击事件处理 =====================


def on_click(x, y):
    global selected, win

    if win:
        return

    click_tower = None
    for i in range(TOWER_COUNT):
        if abs(x - TOWER_X[i]) < 80:
            click_tower = i
            break

    if click_tower is not None:
        if selected is None:
            if towers[click_tower]:
                selected = click_tower
        else:
            if move_disk(selected, click_tower):
                win = check_win()
            selected = None

    redraw()


# ===================== 启动游戏 =====================
screen.onclick(on_click)
redraw()
screen.mainloop()

请将上述代码保存为 hanoi_turtle.py\text{hanoi\_turtle.py} hanoi_turtle.py。用以下命令可以运行 hanoi_turtle.py\text{hanoi\_turtle.py} hanoi_turtle.py

bash 复制代码
python3 hanoi_turtle.py

初始效果如下图所示 ⬇️

运行中的效果如下图所示 ⬇️

胜利时的效果如下图所示 ⬇️

最小移动次数分析

相较于汉诺塔的实现,我更关心其背后的数学知识。我在 具体数学(英文版第2版) 一书第 11 1 章 Recurrent Problems\text{Recurrent Problems} Recurrent Problems 中的 1.11.1 1.1 小节 THE TOWER OF HANOI\text{THE TOWER OF HANOI} THE TOWER OF HANOI 里,看到了相关的分析。按照我的理解,转述如下(有兴趣的读者朋友,可以看看原文)。

我们将汉诺塔问题进行一般化:记盘子的数量为 nn n( nn n 是正整数),三根柱子分别称为 A,B,CA,B,C A,B,C,我们想把 nn n 个盘子从 AA A 移动到另一个柱子 BB B 或者 CC C,将所需要的最小移动次数称为 Tn T_n Tn。

先观察最简单的情况。

  • n=1n=1 n=1 时,把这个盘子从 AA A 移动到 BB B 或者 CC C 即可,所以 T1=1 T_1=1 T1=1。
  • n=2n=2 n=2 时
    • 只操作一次,肯定不行(因为两个盘子至少都得移动一次)
    • 只操作两次,也做不到(暴力枚举即可,因为可行的操作非常有限)
    • 操作三次是可以的(如果目标柱子是 CC C,那么可以这样操作:较小的盘子 A→BA\to B A→B,然后较大的盘子 A→CA\to C A→C,然后较小的盘子 B→CB\to C B→C),所以 T2=3 T_2=3 T2=3。

nn n 更大时,我们总是可以使用如下的三大步策略(假设目标柱子是 CC C)。

  1. 将除了最大盘子之外,其他 n−1n-1 n−1 个盘子从 AA A 移动到 BB B (通过 Tn−1 T_{n-1} Tn−1 次移动可以做到)
  2. 将最大的盘子从 AA A 移动到 CC C (通过 11 1 次移动可以做到)
  3. 将除了最大盘子之外,其他 n−1n-1 n−1 个盘子从 BB B 移动到 CC C (通过 Tn−1 T_{n-1} Tn−1 次移动可以做到)

因为我们这个策略只是可行策略之一,所以可以得出以下不等式 ⬇️

所以 Tn≤ Tn−1 +1+ Tn−1 T_n \le T_{n-1} + 1 + T_{n-1} Tn≤Tn−1+1+Tn−1,即 Tn≤2 Tn−1 +1 T_n \le 2 T_{n-1} + 1 Tn≤2Tn−1+1

至此,我们得到了 Tn T_n Tn 的上界。那么这个策略是不是最优的策略呢?(即,这个策略所需要的移动步数是最少的吗?)。我们来分析一下。

对任意一个成功将 nn n 个盘子从 AA A 移动到 CC C 的完整流程而言,这个流程一定需要移动最大的那个盘子 Pbiggest P_{biggest} Pbiggest。那么以下推断都成立 ⬇️

  1. 在第一次移动 Pbiggest P_{biggest} Pbiggest 之前,我们已经将其余 n−1n-1 n−1 个盘子移动到 BB B 或者 CC C 上了(按照规则,一个盘子在移动时,其上方不能有其他盘子)
  2. 我们至少需要移动 Pbiggest P_{biggest} Pbiggest 一次(否则这个盘子不可能到目标柱子上)
  3. 在最后一次移动 Pbiggest P_{biggest} Pbiggest 之后,我们一定需要将其余 n−1n-1 n−1 个盘子移动到 CC C 上

所以,对这个完整流程而言,它的移动次数至少是 Tn−1 +1+ Tn−1 =2 Tn−1 +1 T_{n-1} + 1 + T_{n-1}=2T_{n-1}+1 Tn−1+1+Tn−1=2Tn−1+1。

于是我们得到 Tn≥2 Tn−1 +1 T_n \ge 2 T_{n-1} + 1 Tn≥2Tn−1+1 (即, nn n 个盘子时的最优操作数至少是 2 Tn−1 +1 2T_{n-1}+1 2Tn−1+1)

既然以下两个不等式都成立
Tn≤2 Tn−1 +1 T_n \le 2 T_{n-1} + 1 Tn≤2Tn−1+1
Tn≥2 Tn−1 +1 T_n \ge 2 T_{n-1} + 1 Tn≥2Tn−1+1

那么,可以得出 Tn=2 Tn−1 +1 T_n = 2 T_{n-1} + 1 Tn=2Tn−1+1。

利用高中时学的数列小技巧,我们可以推出 Tn T_n Tn 的通项公式。具体过程如下 ⬇️

Tn=2 Tn−1 +1 T_n = 2 T_{n-1} + 1 Tn=2Tn−1+1 这个等式两边各自加 11 1,得到
Tn+1=2( Tn−1 +1) T_n + 1 = 2 (T_{n-1} + 1) Tn+1=2(Tn−1+1)

Un=Tn+1 U_n=T_n+1 Un=Tn+1,那么 Un U_n Un 是以 22 2 为首项, 22 2 为公比的等比数列,所以 Un U_n Un 的通项公式为
Un=2n U_n=2^n Un=2n

那么 Tn=Un−1=2n−1 T_n=U_n-1=2^n-1 Tn=Un−1=2n−1。也就是说,当 AA A 柱子上有 nn n 个盘子时,我们需要 2n−12^n-1 2n−1 次移动操作,才能将这 nn n 个盘子移动到 BB B 柱子上(或者 CC C 柱子上)。

相关推荐
今天AI了吗4 小时前
【AI】一文讲清 RAG:从大模型局限到企业级知识库落地流程
前端·javascript·人工智能·python·深度学习·机器学习·easyui
“AI国潮设计-小江”4 小时前
《Python实战 | 用SDXL大模型生成“潮汕英歌舞”国潮IP头像,已申请外观专利,附Prompt思路!》
人工智能·python·prompt·aigc·scikit-learn
霸道流氓气质4 小时前
Spring AI提示词模板与动态变量替换
java·python·spring
民乐团扒谱机4 小时前
【超详细】PyQt6 实现 AU 风格波形图编辑器:多级缩放与采样点逐级渲染,附全过程代码
开发语言·python·编辑器·pyqt·audition·时域·频谱图
CTA量化套保4 小时前
先跑清楚小流程,再让量化功能变复杂
人工智能·python
Csvn4 小时前
🐍 Day 10: 依赖管理 — 从 requirements.txt 到 pyproject.toml
后端·python
2601_962294054 小时前
Python 操作 Word:如何查找、替换和批量修改文本
python·正则表达式·word·文档处理·查找替换
weixin_440730505 小时前
allure总结--解决版本不相容,没有trend(生成原始报告+复制history后在一起生成新报告)+jenkins集成
python·测试报告·allure
小柯南敲键盘5 小时前
跨马翻译:图片翻译软件批量处理,跨境电商视频字幕AI智能抠图
人工智能·python·音视频
BYSJMG5 小时前
计算机毕业设计选题推荐|【基于大数据的植被光谱特征与环境因子关联分析及可视化】Spark+K-Means
大数据·python·信息可视化·数据分析·spark·kmeans·课程设计